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.
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.
Online Assessment
reportedMost 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
reportedInput 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
reportedThis round is deciding whether a change you make without supervision can be allowed to reach production. It is scored on what you knew at the moment you decided, not on how it turned out, so a story that opens with the result and works backwards reads as luck retold as judgement. Say what the options were, what you did not know, what you did to shrink the unknown before committing, and what you accepted as the worst plausible case. The detail that separates answers is a bound: how many users, how much data, and for how long, if you had been wrong.
What to demonstrate
- Whether the reasoning you give was available at the time you decided rather than after the result came in, since a story whose deciding evidence arrived later describes an outcome and not a judgement
- Whether you can put units on the exposure (users, rows, minutes of degraded service) and whether the containment you chose actually bounded it: a canary bounds the request path it fronts, while a background job writing to a shared table reaches every user regardless of which version served their requests
- Whether the reversal path existed before you shipped or was improvised during the incident, and whether it restores state or only stops further damage
How to prepare
- For your three largest changes, write down the one thing you would have had to be wrong about for it to fail, and what your best estimate of it was on the day you shipped. If you never held an estimate, that is the gap the follow-up questions will find
- Write the undo procedure for one of those changes as it existed at the time, then mark which steps restore data and which only stop new damage. Turning a flag off or reverting a deploy ends the new writes; rows already written come back only from a copy you kept, and a dropped column comes back empty unless something outside the schema holds the values
- Rehearse one story from the decision point forward and stop before the outcome, then have someone ask what you would do next. If the story only works with the ending attached, it is an anecdote rather than a decision you can defend
1 candidate reports. Individual accounts describe a particular role and hiring cycle.
Booking Software Engineer Interview Experience — Priceline Canada Screen with AI and Java
One hour. It started with a conversation about my background and previous work experience, followed by AI-related questions: how I normally use AI tools, how I would design AI into a real workflow, how I would plan an AI workflow, and how to use and prompt an AI agent, including skills, hooks, and similar things. The coding portion used Java. The interviewer provided template code first. The firs…
Read full experiencePracHub editorial advice for the preparation topics above.
Implementing an amendment as a cancel followed by a rebook
On a sold-out date, releasing the old nights first hands the unit to a concurrent booker and the traveller's own amendment fails, leaving them with nothing; taking the new nights first double-counts the overlap and can fail the capacity check against the traveller's own existing booking. Either ordering also detaches the amendment from its policy snapshot, so the rebooked stay silently acquires today's cancellation terms and today's price rather than the ones that were agreed. Compute the amendment as a per-night delta inside one transaction: release only the nights being dropped, take only the nights being added, leave the overlap untouched, and derive the payment adjustment from the stored policy snapshot rather than from current prices.
Waiting on every supplier in the fan-out
If N connectors are independent and each exceeds its own 99th-percentile latency one percent of the time, the probability that at least one of them does is 1 minus 0.99^N, which is roughly 18 percent at N equals 20. A request that waits for all of them therefore inherits a tail that no amount of optimising any single connector will fix, and the usual instinct of raising the timeout makes it strictly worse. The workable design is a total deadline budget for the request, per-call timeouts derived from the budget remaining at the moment the call is issued rather than fixed constants, partial results returned with an explicit completeness flag, and a circuit breaker so a supplier that is already down stops consuming budget on every subsequent request. The independence assumption is itself worth checking, because shared infrastructure makes connector latencies correlate exactly when you need them not to.
Hardcoding to the sample inputs
Solve the stated problem rather than the two examples; special-casing a literal to make a sample pass is obvious immediately and reads as either a misunderstanding or an attempt to fake progress. If you genuinely cannot generalise yet, say which part is a stub and what would replace it.
Saying 'eventually consistent' without naming the anomaly a user would see
Describe the concrete symptom you are choosing to accept: the author reloads and their own comment is missing for two seconds, or two devices show different balances for a minute. The class of consistency model is a technical label; the tolerable anomaly is the actual product decision.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Given an unsorted collection of travel legs, reconstruct the complete …
Given an unsorted collection of travel legs, reconstruct the complete trip itinerary from start to finish.
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
- 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 …
Design an in-memory key-value store supporting basic read, write, and time-based eviction operations.
Approach
- Walk one small example through your approach before writing the whole thing.
- Restate the input: its shape, its size, and what is guaranteed about it.
- Choose the data structure from the access pattern, not from familiarity.
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, …
Process an array of financial transactions containing currency codes, amounts, and transaction types (PAYIN or PAYOUT), calculating total net balances and conversion requirements.
Approach
- Choose the data structure from the access pattern, not from familiarity.
- 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?
Build a lightweight component that executes an API request to fetch dy…
Build a lightweight component that executes an API request to fetch dynamic pricing data and renders the response cleanly in UI views.
Approach
- Restate the input: its shape, its size, and what is guaranteed about 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
- What is the worst case, and how likely is it on real data?
- Which test case would catch an off-by-one here?
Apply a full refresh as a set difference over the horizon
A supplier sends a full refresh for unit_type_id = U covering the inclusive horizon [H0, H1], at most 500 dates, carrying one supplier_seq for the whole message. You hold stored ari_daily rows for U, each with its own supplier_seq. Classify every date in the horizon into upsert, withdraw, or skipped-by-guard: a date present in the refresh is upserted only if the message seq exceeds that row's stored seq; a date absent from the refresh is withdrawn only under the same condition. Reject refresh rows outside the horizon and report them. Target O(n + m).
Approach
- Walk both sides with two pointers in
stay_dateorder — they usually arrive sorted, and if not, sorting 500 dates is negligible. This gives O(n + m) time and O(1) working space beyond the output, and it preserves date order in the output so the horizon can be streamed. A hash map of stored rows is the same asymptotic time but costs O(n) space and loses that ordering. - Each pointer step resolves into exactly one of three cases. Date in both sides: upsert if
message_seq > stored_seq, otherwise record skipped-by-guard. Date only in the refresh and inside the horizon: insert. Date only in storage and inside the horizon: withdraw ifmessage_seq > stored_seq, otherwise skip. - Assert the accounting as part of the algorithm, not as an afterthought: every date in
[H0, H1]lands in exactly one of upsert, withdraw or skipped, and the three counts sum to the horizon length. That identity catches a boundary error at H0 or H1 and catches a date processed twice, both of which are otherwise invisible until a guest is turned away. - Reject refresh rows outside
[H0, H1]and report them rather than applying them. Applying them means the message's implied withdrawal set no longer matches the horizon it declared, and the next refresh will disagree with this one about which dates it owns. - Keep the guard per row even though the seq is per message. A date whose stored seq already exceeds the message seq has been superseded by a later delta, and treating the message as newer than everything clobbers it and re-opens whatever that delta closed. This is the one place where the set-difference framing and the ordering guarantee interact, and it is where implementations get it wrong.
- Express a withdrawal as
remaining_units = 0withstop_sell = true, not as a row delete. Deleting discards the seq, so the next out-of-order delta for that date has nothing to compare against and applies unconditionally, which reopens inventory the supplier withdrew.
Worked solution 35 min
- Set the horizon to 2026-04-01 through 2026-04-06 inclusive, six dates, with a message seq of 500.
- Stored rows for U: 04-01 seq 410, 04-02 seq 620, 04-03 seq 400, 04-04 seq 430, 04-05 seq 380. No stored row for 04-06.
- Refresh contents: 04-01, 04-03, 04-05, 04-06.
- Walk the two sorted date streams with two pointers and classify each of the six horizon dates, recording the reason for each.
- Check the accounting identity, then re-run the same fixture as a plain row-by-row upsert and note which date comes out different.
Follow-up
- The refresh is delivered twice. Show that the second delivery produces no writes, and name the property that makes that true.
- The message is split across three chunks that can arrive in any order. What has to change about the horizon and the accounting assertion?
- The search path is reading this unit type's range while the refresh is being applied. What can a reader observe mid-apply, and does that matter more or less than the apply's duration?
Reconcile night revenue against captures without fanning either out
booking(booking_id, unit_type_id, state, total_minor, currency_code) has booking_night(booking_id, stay_date, unit_type_id, night_price_minor, tax_minor, fee_minor, state) one-to-many, and payment_transaction(payment_transaction_id, booking_id, kind, state, amount_minor) one-to-many. A report joins all three and sums, and on a three-night booking with a deposit plus a balance capture both totals are wrong. Name both multiplications precisely, then write the query giving nights sold, night gross and net captured per property per stay month, and state how a booking-grain capture is attributed to the months its nights fall in.
Approach
- Name the cardinality: booking to booking_night is one-to-N and booking to payment_transaction is one-to-M, so joining both produces N times M rows per booking. Every night is counted M times and every payment N times, and the two columns are inflated by different factors, which is why the report looks plausible rather than obviously broken. The defect is aggregating a measure across a row set two children have expanded, not the SUM itself.
- Reject the reflexive fixes. DISTINCT and SUM(DISTINCT ...) de-duplicate values rather than rows, so two genuine nights priced identically collapse into one and the total becomes wrong in the other direction while looking tidier.
- Aggregate each child in its own CTE before joining anything, and filter on kind and state rather than on sign: the capture total is SUM(amount_minor) FILTER (WHERE kind = 'capture' AND state = 'succeeded') minus the same expression for refunds, grouped by booking_id. Authorisations are not revenue and 'unknown' is not a failure, so neither may be counted. SUM with FILTER returns NULL rather than zero when nothing matches, so wrap both sides in COALESCE before subtracting or a booking with no refund reports nothing at all.
- Face the grain mismatch instead of joining through it. Nights are the consumption grain and a capture is at booking grain, so no join makes them directly comparable. Either report the two at their own grains and reconcile explicitly, or allocate each booking's net capture across its nights in proportion to night gross, in integer minor units, distributing the remainder largest-remainder first with a stable tiebreak on stay_date.
- Take the month from booking_night.stay_date and never from booking.created_at, because one booking's nights routinely fall in two months and two tax periods. date_trunc over a date argument returns a timestamp, so cast back to date if the grouping key is meant to be one.
- Filter on booking_night.state rather than booking.state, since a booking can be partially cancelled and the night rows are where that is recorded. Assert that the sum of night price, tax and fee equals booking.total_minor per booking as a test, because per-night, per-jurisdiction rounding is exactly where that identity breaks.
Follow-up
- A refund lands two months after the stay. Which month's net moves, and does finance want the same answer as the payout file?
- The allocation leaves three minor units unassigned on a zero-decimal currency booking. Where do they go, and is the rule stable across re-runs?
- From the result alone, how would you prove that no booking was expanded by a join?
Add a held-units column and constraint to a live calendar
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.
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?
Design a dynamic currency exchange and payout fee calculation module s…
Design a dynamic currency exchange and payout fee calculation module supporting custom conversion rules and transaction ordering.
Approach
- Clarify what is being asked and what a complete answer contains.
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Design a high-throughput credit card fraud detection system capable of…
Design a high-throughput credit card fraud detection system capable of evaluating transactions in real time with minimal latency impact.
Approach
- State your assumptions explicitly before working the problem.
- Say what you would check first and why it is the highest-information step.
- Work from the requirement backwards to the design.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
Rate-limit a search API whose traffic is mostly automated
Partner integrations and scrapers share one search endpoint; searches outnumber bookings by two to three orders of magnitude, and a request with plus-or-minus three days of check-in flexibility evaluates seven candidate date ranges rather than one. Design the limiting contract: what the limit is applied to, the algorithm and its parameters, how an expensive request is charged, the headers a client reads, the status codes and what distinguishes them, and the exact behaviour you expect from a well-behaved partner on refusal. State what must stay reachable while search is being shed.
Approach
- Limit cost, not request count. Charge in units proportional to the (unit_type, date-range) evaluations a request causes — flexibility multiplies candidate check-in dates, candidate unit types multiply again — and publish the formula, because a partner cannot shape traffic against a number it cannot predict. Under a flat request limit, one flexible-search client consumes the fan-out budget at a fraction of the request count.
- Choose the algorithm against the failure you care about. A fixed window admits close to twice the limit across a boundary; a sliding window log is exact but costs memory proportional to requests retained per key; a token bucket costs two numbers per key and lets you state sustained rate and burst size separately, which is the thing you actually want in a contract. Publish refill rate and bucket depth rather than burying them in config.
- Add a concurrency limit beside the rate limit — they bound different resources. A client inside its rate can still pin every connector worker with two hundred simultaneous flexible searches, and only a cap on in-flight requests bounds that.
- Be exact about headers. Retry-After (RFC 9110 §10.2.3) is mandatory on a refusal because it is the only hint every client already implements. The RateLimit-* fields are an IETF draft whose shape has changed between revisions, so pin the revision you implement and document it instead of assuming a partner's SDK agrees with yours. Use 429 for 'you exceeded your quota' and 503 with Retry-After for 'the service is shedding' — they call for different client behaviour and should not share a code.
- Specify the client algorithm rather than hoping for one: retry after the stated delay with full jitter, never with a synchronised fixed backoff, which reconverges every rejected client on the same instant and reproduces the burst you just shed. Publish whether a refusal consumes quota — it should not — so the partner's own accounting can agree with yours.
- Shed in dependency order and say so in the contract. Booking, servicing and the rate-and-availability ingest path need budgets separate from search, because a system that throttles confirms while scrapers are busy has converted a traffic problem into lost bookings the traveller had already consented to.
Worked solution 25 min
- Compute the unit cost of three concrete requests: one property on fixed dates, plus-or-minus three days across three unit types, and a wide destination search.
- Pick bucket depth and refill rate from one real partner's traffic shape and state the burst that allows.
- Write the 429 and 503 responses including headers, and the precise client retry algorithm you expect against each.
- Draw the shedding order across search, booking, servicing and ingest, naming the separate budget each one holds.
Follow-up
- A partner's key leaks and their traffic quadruples. What in this design bounds the blast radius without cutting off their legitimate calls?
- Where does the counter live, and what happens when that store is unavailable — fail open or fail closed?
- How does the cost formula stay honest once you add a cheaper cached path for popular dates?
Multi-night holds deadlock during a flash sale
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?
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.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Rebuild 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.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Arrays 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.
Practice prompt ↗Practice prompt ↗03Sorting, 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.
Practice prompt ↗Practice prompt ↗04Recursion, 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.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Graphs, 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.
Practice prompt ↗Practice prompt ↗06One 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.
Practice prompt ↗Practice prompt ↗07Solve 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.
Practice prompt ↗Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
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.
How do you handle situations where product requirements are ambiguous,…
How do you handle situations where product requirements are ambiguous, or when technical trade-offs conflict with aggressive timelines?
Approach
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
- 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?
Tell me about a time you made an architectural decision that failed or…
Tell me about a time you made an architectural decision that failed or produced unexpected side effects, and how you mitigated the issue.
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Pick a story where you made the decision, not one where you watched it.
- 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?
Reverse your own decision on hold expiry
You chose sweeper-only expiry for inventory_hold_night: a job flips lapsed rows from active to expired and returns units, and the availability read trusts the state column alone. Production contradicted the choice. Describe a decision you reversed: what you originally believed and why it was reasonable, the specific observation that changed your mind, how long you waited before reversing, what the reversal cost, and what you kept from the first design. Name the metric that moved and one that did not.
Approach
- Say what made the original choice reasonable, because a reversal story with no defensible starting point reads as carelessness rather than judgement. Sweeper-only expiry keeps the read path trivial and lets the partial index on (unit_type_id, stay_date) WHERE state = 'active' answer availability with no clock in the query.
- Give the observation that broke it, in numbers. When the sweeper falls behind by L seconds, up to L seconds of lapsed holds still read as active, so availability is understated and searches lose bookable inventory on exactly the dates where holds are densest, which are the dates that sell. Availability understatement is the metric that moves first.
- Separate it from the opposite failure so the reversal is not mistaken for a panic. A sweeper that releases a hold the booking path has already converted increments capacity back and oversells, which is why the release must be conditional on the active-to-released transition and not a bare increment. Both failures are real and they pull in different directions.
- State the reversal concretely: evaluate expiry against the clock at read and write time, filtering active rows by expires_at_utc, and keep the sweeper for cleanup. Name the constraint that shapes it, namely that now() is not immutable so 'not expired' cannot be a partial index predicate; expiry is always a filter over the active set, which is exactly why the sweeper has to keep that set small.
- Report the cost honestly: a re-tested read path, a migration of in-flight holds, and a measurable increase in rows filtered per availability query. Then name a metric that did not move, because a candidate who claims everything improved is not measuring.
- Close on how long you waited and what you would watch earlier next time. Waiting for two data points beats reversing on one anecdote; waiting for a quarter is a different mistake.
Follow-up
- What is the right TTL for a hold, and what evidence would set it rather than a round number?
- How do you stop the sweeper from releasing a hold that the booking path converted a millisecond earlier?
- If the sweeper falls two minutes behind under load, what does a traveller see and what does the revenue graph show?
- 01
How do you handle situations where product requirements are ambiguous, or when technical trade-offs conflict with aggressive timelines?
- 02
Tell me about a time you made an architectural decision that failed or produced unexpected side effects, and how you mitigated the issue.
- 03
You chose sweeper-only expiry for inventory_hold_night: a job flips lapsed rows from active to expired and returns units, and the availability read trusts the state column alone. Production contradicted the choice. Describe a decision you reversed: what you originally believed and why it was reasonable, the specific observation that changed your mind, how long you waited before reversing, what the reversal cost, and what you kept from the first design. Name the metric that moved and one that did not.
Is this an official Booking.com interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Booking.com. Rounds and questions reflect what candidates have reported, not a process Booking.com has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How difficult are the technical interviews at Booking compared to other major tech companies?
The technical bar is comparable to top-tier international tech organizations. While algorithmic coding problems focus heavily on medium-level data structures applied to realistic scenarios, system design rounds are particularly rigorous and emphasize scalable microservices and operational resilience.
PracHub interview research ↗Can I complete the coding rounds in any programming language?
Yes. You can generally select your preferred general-purpose programming language (e.g., Java, Python, C++, JavaScript, Go) for algorithmic and coding rounds. However, specialized roles (such as iOS, Frontend, or Salesforce specialists) will require coding in domain-specific languages.
PracHub interview research ↗What is the primary focal point during the system design interview?
Interviewers evaluate your ability to create a well-structured, modular system that meets non-functional requirements such as high availability, fault tolerance, low latency, and scale. Clear API definition, database choices, caching strategies, and message queuing logic are vital.
PracHub interview research ↗Does Booking offer relocation assistance for international hires?
Yes. For roles based in major hubs like Amsterdam or Manchester, Booking frequently provides full relocation packages and visa sponsorship support for qualified candidates, making international recruitment a standard part of their talent strategy.
PracHub interview research ↗Sources & methodology 3 sources ↗
No official company page is cited. Rounds and questions come from candidate reports and PracHub editorial material; each source shows the date it was read.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
PracHub page · Accessed 2026-10-14 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
PracHub page · Accessed 2026-10-14 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
PracHub page · Accessed 2026-10-14