Interview guides · Booking
Booking Software Engineer Interview Questions & Guide 2026
Booking software engineer prep on partial failure: a timed-out request has an unknown outcome, not a failed one.
The 60-second brief
As a Software Engineer at Booking, you operate at the core of one of the world's largest travel e-commerce platforms. Your work directly impacts millions of travelers and partner properties worldwide, enabling seamless travel experiences across accommodations, flights, rental cars, and connected trips. Engineers at Booking build and scale backend systems, financial payment pipelines, real-time availability engines, and user-facing interfaces that process immense transactional volume with high availability and low latency.
State every complexity claim with the assumption sitting under it. Hash lookup is O(1) on average and only for a hash that spreads your actual keys; comparison-based sorting cannot beat n log n, though counting or radix sort can when the keys are bounded integers.
Focus areas
- Derive idempotency keys from attempts, never from retries
- Bound search fan-out with an explicit deadline budget
- Decrement per-night inventory with one conditional update
Overview
The engineering organization at Booking emphasizes autonomy, empirical testing, and production-grade software craft. You will routinely tackle complex distributed systems challenges—such as synchronizing property availability across global partner networks, orchestrating multi-currency payin and payout workflows, or designing real-time fraud detection systems. Working alongside cross-functional teams of product managers, data scientists, and infrastructure specialists, you will deploy code into production environments that demand high fault tolerance and sub-second response times.
Whether you join as a backend, full-stack, or specialized mobile engineer, your contributions directly affect the business's bottom line. At Booking, software engineering is not merely about writing clean algorithms; it is about delivering measurable business value, maintaining continuous uptime during global traffic spikes, and continuously improving a massive microservices architecture.
The Booking interview process
Booking candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Online Assessment
Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Technical Interviews
Input bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.
What to demonstrate
- Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
- Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
- Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
- Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply
How to prepare
- For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
- For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
- Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last
Behavioral Interviews
Many of these questions are about something that went wrong, and the grading sits mostly in the hours after you knew. Who found out first, whether that was you or an alert or a user, how long it took you to say it out loud, and whether the people who needed the news got it while they could still act on it. Engineers under-tell this part because it feels like confessing. The pattern it is looking for is the opposite: the quiet fix, an incident absorbed without telling anyone, after which nothing changed and the same failure is still available.
What to demonstrate
- How the problem was found, and whether that route was one you had built or one that happened to you, since a user reporting it first means your instrumentation did not cover that failure
- Whether time-to-detect and time-to-tell are separate numbers in your account and whether you know both, because a fast fix that nobody heard about until the retro is a different answer from a slow one that was announced immediately
- Whether the resolution left something durable behind, a check that fires or a default that changed, rather than depending on people remembering to be careful
- Whether you can say what the failure cost without either inflating it or waving it away
How to prepare
- Reconstruct one incident you were part of as a timeline with clock times: first bad request, first signal, first person who knew, first message outside the team, mitigation, permanent fix. The gaps between those entries are what gets asked about
- Look up the configuration of the signal that caught it, including its evaluation window and threshold. An alert defined on a five-minute aggregate cannot fire until the condition holds across that window, which puts a floor under time-to-detect that has nothing to do with how severe the failure was. Be able to say what that floor was and whether anyone had chosen it deliberately
- Prepare one story where you escalated early and the severity turned out to be smaller than you thought, including what it cost the people you pulled in. Without it, every answer you give about raising alarms is unfalsifiable
Where candidates lose points
Caching search results at the (property, date range, party) grain
That key is the cartesian product of destination, check-in, length of stay, party composition, currency, point of sale and promotion eligibility, so the hit rate is close to zero on anything but the most popular routes and the cache pays for itself only in memory. Worse, any input you forget to include in the key leaks one traveller's price to another, and price leaks of that kind are discovered by customers rather than by monitoring. Cache at the (unit_type, stay_date) grain instead: the key space is bounded by unit types times the forward horizon, every entry is reused across every length of stay and every flexibility variant that touches that night, and range composition plus restriction evaluation happens at request time on cheap in-memory rows. The tradeoff is that invalidation now has to be precise per unit-date, which is the right problem to have.
Modelling availability as a boolean
Bookability is a count plus a set of per-night restrictions whose scopes differ, and collapsing that into is_available breaks in both directions at once. Closed-to-arrival applies only to the arrival night; closed-to-departure applies only to the checkout date, which is not itself a booked night; stop-sell applies to every night in the range; minimum length of stay is conventionally read from the arrival night's row, though implementations genuinely differ and that is the first thing to pin down with a supplier. A boolean drops stays that are legal (a range whose middle night is closed to arrival is perfectly bookable) and sells stays that are not, and the second kind does not fail in search, it fails at supplier confirm after the traveller has been charged.
Arguing past a hint
When the interviewer asks what happens for a particular input or floats a different data structure, stop and take it seriously; it is almost always a correction rather than idle curiosity. Talking over it converts a recoverable wrong turn into a data point about how you handle review.
A queue or buffer with no bound
Every producer-consumer boundary needs a capacity and a policy for reaching it: block the producer, shed load, or drop the oldest entry. Unbounded buffering converts a temporary slowdown into memory exhaustion and hides the backpressure signal that would have revealed the consumer was falling behind.
Questions & practice
Given an unsorted collection of travel legs, reconstruct the complete trip itinerary from start to finish.
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Walk one small example through your approach before writing the whole thing.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- Which test case would catch an off-by-one here?
- What is the worst case, and how likely is it on real data?
Design an in-memory key-value store supporting basic read, write, and time-based eviction operations.
Medium · data structures and algorithms
Approach
- Walk one small example through your approach before writing the whole thing.
- Choose the data structure from the access pattern, not from familiarity.
- State the target complexity and say which constraint rules the naive version out.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Process an array of financial transactions containing currency codes, amounts, and transaction types (PAYIN or PAYOUT), calculating total net balances and conversion requirements.
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Walk one small example through your approach before writing the whole thing.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- Which test case would catch an off-by-one here?
- What is the worst case, and how likely is it on real data?
Build a lightweight component that executes an API request to fetch dynamic pricing data and renders the response cleanly in UI views.
Medium · data structures and algorithms
Approach
- Restate the input: its shape, its size, and what is guaranteed about it.
- Name the brute-force solution and its complexity before improving on it.
- State the target complexity and say which constraint rules the naive version out.
Follow-up
- Which test case would catch an off-by-one here?
- What is the worst case, and how likely is it on real data?
Reconcile booking totals against night-grain amounts
Easy · hash aggregation · reconciliation · minor units
Two unsorted inputs. booking(booking_id, total_minor, currency_code) has up to 2,000,000 rows. booking_night(booking_id, stay_date, night_price_minor, tax_minor, fee_minor, state) has up to 10,000,000 rows, state in held, confirmed, cancelled, consumed, no_show. For every booking the sum of night_price_minor + tax_minor + fee_minor over its non-cancelled nights must equal total_minor. Report every booking_id where it does not, with the signed difference in minor units, and separately report booking_ids present in only one input. Target O(N + B) time. State your space bound.
Approach
- Stream
booking_nightonce into a hash map keyed by booking_id, accumulatingnight_price_minor + tax_minor + fee_minorin a signed 64-bit integer and contributing zero for rows in statecancelled. Create the entry on every night row, cancelled ones included: that is what keeps a booking whose nights are all cancelled distinguishable from a booking with no night rows at all. Minor-unit totals sit far below the int64 ceiling of about 9.2 x 10^18, so no decimal or big-integer type is needed, and keying strictly by booking_id is also what keeps two currencies from ever landing in one accumulator. - Stream
bookingonce, look up each booking_id, emit the signed difference when it disagrees withtotal_minor, and remove the map entry as you consume it. - What is left in the map afterwards is nights-without-a-booking; bookings that found no entry are bookings-without-nights. Both are one-sided keys and both are real defects, so the report iterates both key sets. A booking whose nights were all cancelled has a legitimate sum of zero against a non-zero total, and it only appears if you walk the booking side.
- Time O(N + B), space O(B): roughly 2 x 10^6 int64 accumulators plus keys, tens of megabytes, which fits one process. If B outgrows memory, hash-partition both inputs on
hash(booking_id) mod Pand run the identical algorithm per partition — every key lands in exactly one partition, so correctness is unchanged and peak memory falls by P. - Trade-off worth stating: the single-pass version needs the nights side to fit in memory as an aggregate, while a sort-merge on booking_id needs only streaming memory but costs O(N log N) comparisons. At N = 10^7 the hash version wins; the sort version is the answer when the nights side is a hundred times larger.
Worked solution (20 min)
- Take four bookings. B1 total 30000 with three non-cancelled nights of 9000 + 900 + 100 each. B2 total 20000 with two nights of 10000, one of them state
cancelled. B3 total 15000 with two nights, bothcancelled. B5 total 8000 with no night rows at all. Separately, non-cancelled night rows for B4 summing 12000 with no booking row — B4 is not one of the four bookings. - Run the nights pass and write down the map: B1 -> 30000, B2 -> 10000, B3 -> 0, B4 -> 12000. B3 has an entry holding 0 because its cancelled rows created the key and contributed nothing; B5 has no entry at all.
- Run the bookings pass, emitting
sum - totalfor each mismatch and deleting entries as you go. - Read off whatever is left in the map and report it as nights-without-booking.
- Sum the signed differences as a single checksum.
Expected result: Three of the four bookings are reported: B2 at -10000, B3 at -15000, B5 at -8000. B1 is clean and absent. B4 has no booking row and appears only on the nights-without-booking list with 12000. The signed differences sum to -33000.
Verify it yourself
- B1 must not appear anywhere in the output; a clean booking that shows up means the cancelled-state filter is inverted.
- B3 and B5 both report a sum of zero but arrive by different routes — B3 through a present-but-zero map entry, B5 through a missing one. If either is absent, one of the two key sets is not being walked.
- The reported differences sum to exactly -33000, computed in integers; any non-integer intermediate means a float crept into the accumulator.
- Deleting map entries during the booking pass must leave exactly one residual key, B4.
Follow-up
- The report runs while bookings are being written. How do you choose a cut point so a booking mid-write is not reported as a variance?
- Per-night tax rounds per jurisdiction. If the sum of rounded night amounts legitimately differs from the booking total by one or two minor units, is that a variance or a modelling bug, and what do you change?
- How would you extend this to detect a booking whose night rows do not cover the contiguous range [check_in_date, check_out_date) exactly once?
Add a held-units column and constraint to a live calendar
Hard · online migration · lock queue · batched backfill
ari_daily holds about 225 million rows, 500,000 unit types over a 450-day horizon, and is upserted continuously by the ingest path while the search path reads it. You must add held_units SMALLINT NOT NULL DEFAULT 0, a CHECK that remaining_units - held_units >= 0, a unique index the hold path needs, and a backfill of held_units from inventory_hold_night, with no window of unavailability. Give the ordered statement plan, the lock each step takes, and how you stop one long-running query from turning a millisecond DDL into an outage.
Approach
- Order the steps so each is cheap in isolation. Adding a column with a constant default has been a catalogue-only change since PostgreSQL 11 and does not rewrite 225 million rows, while a volatile default such as now() still does — so the constant is the whole trick. Add the column, deploy the code that maintains it, backfill, and only then validate the constraint.
- Add the CHECK as NOT VALID first. That takes a brief ACCESS EXCLUSIVE lock, skips the scan and begins enforcing new writes immediately; VALIDATE CONSTRAINT afterwards takes only SHARE UPDATE EXCLUSIVE and scans without blocking readers or writers. Adding it valid in one step instead scans every row while holding the table exclusively.
- Never add the unique constraint directly, because that builds the index under ACCESS EXCLUSIVE. Use CREATE UNIQUE INDEX CONCURRENTLY, which cannot run inside a transaction block, makes two passes over the table and can leave an invalid index behind if it fails — which must be dropped concurrently and retried rather than ignored. Then adopt it with ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX, which takes a brief exclusive lock and no rebuild.
- Defend every exclusive moment with a lock timeout and a retry loop. A pending ACCESS EXCLUSIVE request queues behind one long-running SELECT, and every later lock request queues behind that, so a reporting query or a session idle in transaction converts a millisecond ALTER into a full stall on the hottest table you own. Set lock_timeout to something like 100ms, retry with backoff, and inspect the activity view for the transaction you are about to queue behind.
- Backfill in bounded batches keyed on the primary key, committing each one. A single UPDATE across 225 million rows writes a new tuple version per row, roughly doubling table size before autovacuum catches up, holds one transaction open for hours, and blocks the ingest path on every row it has already touched. Batch by unit_type_id ranges, pause between batches, and watch dead tuple counts and replication lag rather than wall-clock progress.
- Account for what the column costs per row. New columns are appended and aligned, so a smallint can cost more than two bytes depending on what precedes it, and because every upsert already rewrites the whole tuple, that width is paid on every write to the table from then on rather than once at migration time.
Worked solution (40 min)
- Clone a representative slice, add the column with a constant default, and confirm from timing and table size that no rewrite occurred.
- Add the CHECK as NOT VALID, insert a violating row to prove it is enforcing, then validate it and observe the lock level taken.
- Start a long SELECT in one session and run the ALTER in another with and without lock_timeout, watching the queue build behind it.
- Run the backfill in batches while the ingest path is writing, recording dead tuples and replication lag per batch.
Expected result: An ordered runbook in which no statement holds ACCESS EXCLUSIVE for more than a few milliseconds, the backfill is throttled and resumable, and the constraint is validated only once the column is fully maintained by application code.
Verify it yourself
- Table size and timing confirm the column addition did not rewrite the table.
- The lock view shows a share-update-exclusive lock during validation and during the concurrent index build, not an exclusive one.
- With lock_timeout set, the blocked ALTER fails and retries instead of queueing readers behind it.
- Killing the backfill mid-run and restarting it produces the same final held_units values.
Follow-up
- The concurrent index build fails at eighty percent. What is on disk, what does the planner do with it, and what is your next statement?
- A physical replica serves part of the search traffic. What does replaying this migration do to queries running there?
- How do you make the backfill resumable after it is killed at batch four thousand of five thousand, without rescanning what it already did?
Find bookable unit types for a stay range without scanning
Medium · range predicates · indexing · sargability
Given ari_daily(unit_type_id, stay_date, remaining_units, min_los, max_los, closed_to_arrival, closed_to_departure, stop_sell) with PRIMARY KEY (unit_type_id, stay_date), and inventory_hold_night(hold_id, stay_date, unit_type_id, units, state, expires_at_utc), write one query returning which of 800 candidate unit types are bookable for check-in 2026-03-12, check-out 2026-03-15, two units. Apply each restriction at the night that governs it, subtract active unexpired holds, and reject any range with a missing calendar row. The current draft filters on stay_date alone and plans a sequential scan; say precisely why.
Approach
- Fix the interval before writing anything. Nights are the half-open range, so a three-night stay consumes three rows, while closed_to_departure lives on the checkout row, which is outside that range. Scan stay_date >= check_in AND stay_date <= check_out in one pass and separate the two groups with FILTER, rather than paying for a second round trip.
- Aggregate per unit type and express every condition in HAVING with its own FILTER: COUNT(*) FILTER (WHERE stay_date < check_out) equal to check_out minus check_in proves no night is missing, since subtracting one date from another yields an integer day count in PostgreSQL; bool_and(NOT stop_sell) over the same nights; bool_and(NOT closed_to_arrival) FILTER at the arrival date; bool_and(NOT closed_to_departure) FILTER at the checkout date; min_los and max_los read from the arrival row.
- Know what an empty FILTER yields. bool_and and min over zero rows return NULL, and NULL in HAVING is not true, so the unit type is excluded — correct for a missing stay night, wrong for a missing checkout row, which is routine past the published horizon. COALESCE that one branch to permissive and leave the rest strict; the asymmetry is a decision to state, not an oversight to hide.
- Subtract holds at the night grain. LEFT JOIN a subquery over inventory_hold_night grouped by (unit_type_id, stay_date) with state = 'active' AND expires_at_utc > now(), then require MIN(remaining_units - COALESCE(held, 0)) over the stay nights to be at least the requested units. The COALESCE is load-bearing: a night with no hold rows joins to NULL, and NULL arithmetic removes the unit type without a word.
- Explain the sequential scan by the index's leading column. The primary key is (unit_type_id, stay_date), so a predicate on stay_date alone cannot descend it and the planner falls back to the heap or to reading the whole index. Restoring unit_type_id = ANY(candidates) turns the same index into roughly 800 short range scans of four rows each.
- Check the bound parameter's type before blaming the planner. unit_type_id is bigint; bigint compared with integer is indexable through the integer btree operator family, but bigint compared with numeric is not, so a driver that binds numeric silently converts every one of those range scans into a scan of the table. Cast the parameter, never the column.
Follow-up
- The candidate list is 50,000 unit types for a city-wide search rather than 800. At what point does the primary key stop being the right access path, and what would you measure to find it?
- Add plus or minus three days of date flexibility. How many rows does one page now touch, and does this query shape still hold?
- The hold subquery runs on every request. What does it cost on a popular date carrying thousands of expired hold rows nobody has swept?
Architect an automated payment capture system that splits customer charges across defined schedules (e.g., 50% upfront at booking, 50% two days prior to check-in).
Medium
Approach
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Design a dynamic currency exchange and payout fee calculation module supporting custom conversion rules and transaction ordering.
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Evolve the quote payload without breaking installed clients
Hard · versioning · backward compatibility · money
GET /quotes/{id} returns total_price as a JSON number of cents, available as a boolean, cancellation_policy as free text, and rate_plan from a two-value enum that installed clients switch over exhaustively. You must add per-night, per-jurisdiction tax and fee lines, replace the availability boolean with a remaining count plus the per-night restrictions that decide bookability, add a third rate_plan value, and serve currencies with zero and three minor digits. Eleven percent of weekly traffic is from builds over a year old that cannot be upgraded. Specify the compatibility policy, the wire changes, how both generations are served, and the evidence that retires the old shape.
Approach
- Sort the four changes by what an old client does with each, because that decides the mechanism, not the field. A new tax object is ignored by a tolerant parser and ships additively. The availability boolean is a projection the server still computes, so keep emitting it to old clients meaning exactly what it always meant — this range is bookable for this party — and expose the count and per-night restrictions only in the new shape; the projection stays honest because the evaluation behind it did not change. A third enum value crashes an exhaustive switch. And 'cents' is simply false in a zero- or three-decimal currency, which an old client renders confidently and wrongly.
- Write the policy that was missing when v1 shipped: add, never repurpose or retype; unknown fields ignored; unknown enum values map to a documented fallback; nothing removed until telemetry shows the last consumer is gone. Then accept that it cannot be retrofitted into a binary already on a device. That gap is the whole argument for per-request negotiation rather than a server-side default.
- Negotiate explicitly — a version in the media type or a capability header — keep one internal model behind two serialisers, and write the downgrade rules down. The third rate plan, partially_refundable, renders to old clients as non_refundable rather than refundable, because the safe direction under-promises. Say the loss aloud: those travellers see stricter terms than they hold. The alternative is a crash, so the trade is worth making and worth publishing.
- Do not redefine total_price. Keep populating it as the exact sum of the new night lines for the whole sunset window, and carry the breakdown in a new object with an explicit currency and its ISO 4217 minor-unit exponent. Send the new amounts as strings if any client parses JSON numbers into IEEE-754 doubles, which represent integers exactly only up to 2^53, and assert the night lines sum to the legacy total in a test — otherwise the two drift the first time a per-jurisdiction rounding rule changes.
- Keep zero- and three-decimal currencies away from old clients by construction, not by a runtime check: gate those points of sale to clients that negotiated the new shape, enforced at onboarding. Returning an error to an old client instead is worse — that is an outage for the partner rather than the absence of a feature.
- Retire on evidence. Count requests per negotiated version keyed by install, publish a numeric sunset rule with its announcement lead time, and only then refuse — 426 Upgrade Required is the usual convention here, though the status is specified for protocol upgrade, so the machine-readable code in the body is what clients should branch on. Keep per-version response snapshot tests, because version conditionals threaded through business logic are how a v1 response starts depending on a v3 decision.
Worked solution (40 min)
- Classify each change as additive, breaking-by-crash or breaking-by-silence, and pick the mechanism from that classification.
- Write the negotiation: where the version lives, the default for a request that does not negotiate, and the rejection rule.
- Implement two serialisers over one internal model, including the enum downgrade and the availability projection.
- Add per-version snapshot tests plus a test asserting the night lines sum exactly to the legacy total.
- Define the sunset: the metric, the threshold, the lead time, and the refusal at the end of it.
Expected result: Taxes, a structured policy and a count-plus-restrictions availability model ship with no forced upgrade; the third rate plan reaches old clients as the stricter of the two existing values; zero- and three-decimal currencies are unreachable from the old shape by construction; total_price keeps its original meaning until a dated, numeric sunset removes it.
Verify it yourself
- The year-old build renders a partially refundable quote without crashing, and the terms it shows are stricter than the real ones rather than looser.
- Deleting the legacy total from the internal model breaks exactly one serialiser and no pricing logic.
- A quote in a zero-decimal currency is never served to a client that did not negotiate the new shape, verified by an integration test rather than by code review.
Follow-up
- An old client books against a quote whose rate plan it rendered as non_refundable. What does the write path store, and whose terms govern a later cancellation?
- You now want to delete total_price. What must be true first, and how do you prove it rather than assume it?
- How do you test against the year-old build itself rather than against today's source?
Multi-night holds deadlock during a flash sale
Medium · deadlock · lock ordering · isolation levels
During a promotion the booking path's failure rate rises from 0.1% to 6%. PostgreSQL logs SQLSTATE 40P01 with two statements named per incident, both of the form UPDATE ari_daily SET remaining_units = remaining_units - 1 WHERE unit_type_id = $1 AND stay_date = $2 AND remaining_units >= 1. Single-night stays are unaffected; only stays of two or more nights fail, in pairs, on one popular unit type. The nights are iterated from a hash map built out of the request payload. Deliverable: the interleaving, the fix, and why a retry-only mitigation is insufficient.
Approach
- Read the database's own deadlock report before the application log. The DETAIL names both backends, both statements and the tuples each waits on; confirming that the two statements touch the same (unit_type_id, stay_date) rows in the opposite order is the entire diagnosis and costs one log line.
- Trace the order back to its source. A hash map has no stable iteration order, so two overlapping ranges -- 14-16 March and 15-17 March -- can lock the 15th then the 16th in one request and the 16th then the 15th in the other. Each holds one row and waits on the other until the detector fires at deadlock_timeout and aborts one transaction.
- Fix the order rather than the symptom: sort nights ascending by stay_date and issue one conditional UPDATE per night, inspecting each row count before issuing the next. Do not collapse it into a single UPDATE ... WHERE stay_date = ANY($1): the executor takes row locks in plan order, which you do not control and which can change with statistics, and you also lose the per-night row count the capacity check depends on.
- Keep a bounded retry but change what it means. With a deterministic order, two correctly written hold paths cannot deadlock, so any remaining 40P01 indicates a third writer -- the expiry sweeper, an amendment, a supplier refresh -- not obeying the same order. Alert on it instead of absorbing it.
- State the isolation dependency explicitly, because the retry policy differs by level: under READ COMMITTED a blocked UPDATE re-reads the newly committed row and re-evaluates remaining_units >= 1, so the guard still holds; under REPEATABLE READ or SERIALIZABLE the same statement instead raises SQLSTATE 40001, which the caller must catch and retry with an explicit budget.
Follow-up
- The sweeper that expires holds also writes ari_daily. What order does it use, and what happens to your invariant if it batches its work by hold_id?
- Under SERIALIZABLE, how many retries do you allow, with what backoff, and what do you show the traveller when the budget is exhausted?
Suggested practice checklist
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Day 1 — Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Day 2 — Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Day 3 — Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Day 4 — Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Day 5 — Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Day 6 — One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Day 7 — Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.
'We rewrote it in a better framework' is not a story
A migration is a cost you chose to pay, not an achievement. The story is what the old system made expensive, what you measured before committing, what kept serving traffic during the cutover, and what you would have done if the numbers had come back flat. Without those, a rewrite reads as taste.
Explain what being a "T-shaped" engineer means to you, and share how you have expanded your technical skills outside your primary domain.
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
How do you verify and ensure that your production APIs are operating reliably beyond standard error rate monitoring?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
Resolve a review disagreement over an inventory decrement
Easy · code review · concurrency control · isolation levels
A colleague's pull request reads remaining_units from ari_daily, checks it against the requested units in application code, then issues UPDATE ari_daily SET remaining_units = remaining_units - :n for each night. You believe it must be one conditional UPDATE per night with an explicit row-count check, and that the isolation level has to be a stated decision. Describe a code review disagreement of this kind that you had: what you wrote in the comment, what evidence ended it, what you conceded, and what the engine actually does under the isolation level the code assumed.
Approach
- Write the failing interleaving in the comment rather than a principle. Two requests read remaining_units = 1, both pass the application check, both decrement, and the row lands at -1 or is clamped by the CHECK into a constraint error that surfaces as a 500 rather than as sold out. A four-line trace is harder to wave away than 'this is a race'.
- Give the replacement as a statement, not a description: UPDATE ari_daily SET remaining_units = remaining_units - :n WHERE unit_type_id = :u AND stay_date = :d AND remaining_units >= :n, one per night, inside one transaction, with every statement's row count inspected before commit. Say that the CHECK (remaining_units >= 0) is the last line of defence and not the concurrency control.
- State the isolation behaviour precisely for the engine in play, because this is where reviews go in circles. Under PostgreSQL READ COMMITTED a blocked UPDATE re-reads the row version that the blocker committed and re-evaluates its WHERE clause, so the guard still holds and a losing writer gets zero rows. Under REPEATABLE READ or SERIALIZABLE the same statement instead raises a serialisation failure, SQLSTATE 40001, which the caller must catch and retry with a bounded budget. Those are different code paths.
- Raise the second defect while you are there: the nights must be locked in ascending stay_date order. Two multi-night bookings over overlapping ranges that lock in request order deadlock, and that is a separate bug from the lost update.
- End the disagreement with a runnable artefact. A two-connection test that interleaves the two transactions and asserts the second gets zero rows takes twenty minutes and converts the discussion into a red test, which is the only thing that reliably ends this class of argument.
- Concede what is genuinely arguable. Retry budget, whether to hold or to take inventory at a different step, and whether the allowance applies here are judgement calls; the read-then-write is not.
Follow-up
- The author says the window is microseconds and the traffic is low. What is your answer?
- Where exactly do you put the retry for a 40001, and what is the budget before you give up on the booking?
- How would you write the test so it fails reliably in CI rather than one run in fifty?