cswork / libraryOpen practice workspace

Interview guides · Tripadvisor

Tripadvisor Software Engineer Interview Guide

Tripadvisor software engineer prep: algorithm drills where every complexity claim has to name the assumption under it.

The 60-second brief

As a Software Engineer at Tripadvisor, you will build and scale the global technology infrastructure that connects hundreds of millions of travelers with places to stay, things to do, and places to eat. Operating across major brands including Tripadvisor, Viator, and TheFork, engineering teams at Tripadvisor solve complex marketplace problems spanning high-throughput search, distributed real-time booking, personalization, and multi-tenant content delivery. Your code directly impacts how millions of users discover, plan, and book travel experiences every single day.

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

  • Bound search fan-out with an explicit deadline budget
  • Hold money in minor units, rounding per jurisdiction
  • Derive idempotency keys from attempts, never from retries

Overview

The role demands a balance between technical depth and product-driven execution. Whether you are building low-latency RESTful APIs in Java, optimizing front-end performance in React, designing scalable microservices on AWS, or integrating machine learning models for travel recommendations, you will work in an environment that values clean architecture, continuous delivery, and operational excellence. Senior engineers in this role are expected to demonstrate strong technical leadership, owning end-to-end feature lifecycles from architectural design to post-deployment monitoring.

Joining Tripadvisor means stepping into a collaborative engineering culture where technical rigor is paired with autonomy. Candidates who thrive here possess strong computer science fundamentals, a pragmatic approach to problem-solving, and a focus on delivering measurable user impact at scale.

The Tripadvisor interview process

Tripadvisor candidates report 4 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.

HR Recruiter Screen

Before anything technical happens, someone has to decide which rung of the ladder your loop is calibrated to, and that decision sets the bar for every round after it. It comes from how you describe scope, not from your title, because titles do not convert cleanly between companies. The weak version of the answer is team size and years. The strong version names the largest change you shipped where nobody reviewed the design, what would have broken if you had been wrong, and what you were paged for. Get the level said out loud on this call, because the range and the loop both follow from it.

What to demonstrate

  • Whether the scope in your own account maps onto a level the team actually has an opening at, so a mismatch ends the process cheaply rather than after four interviewers have spent a day
  • Whether your title needs re-mapping: the same word describes very different amounts of independent decision-making at a twenty-person company and a ten-thousand-person one
  • Whether your compensation expectation can be filled at that level in the structure the role pays in, which is why the number gets asked for before any engineer is scheduled

How to prepare

  • Write down two changes from the last two years: the largest one you designed with nobody reviewing the design, and the largest one where someone more senior did. Lead with the first when scope comes up, and be ready to say which parts of the second were yours
  • Ask which level the loop is calibrated to and what changes at the level above it, then plan your weeks from that answer rather than from the posting
  • Settle a total-compensation range beforehand with the split named, base against bonus against equity and its vesting period, so a question about numbers gets a number instead of the word market

Automated Assessment

The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.

What to demonstrate

  • In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
  • In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
  • Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes

How to prepare

  • Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
  • Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
  • For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first

Technical Interviews

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.

Behavioral Interview

This 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

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.

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.

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.

Naming no test cases at all

State what you would test before being asked: empty input, a single element, all elements equal, the maximum permitted size, and the input that exercises the branch you just wrote. It costs thirty seconds and is much of what separates someone who has shipped code from someone who has only solved puzzles.

Questions & practice

Implement an LRU (Least Recently Used) Cache with constant time complexity for get and put operations.

Medium · data structures and algorithms

Approach

  1. Choose the data structure from the access pattern, not from familiarity.
  2. State the target complexity and say which constraint rules the naive version out.
  3. Restate the input: its shape, its size, and what is guaranteed about it.

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?

Given a string containing brackets, determine if the input string is valid and properly balanced.

Medium · data structures and algorithms

Approach

  1. Walk one small example through your approach before writing the whole thing.
  2. State the target complexity and say which constraint rules the naive version out.
  3. Restate the input: its shape, its size, and what is guaranteed about it.

Follow-up

  • How does this change if the input no longer fits in memory?
  • Which test case would catch an off-by-one here?

Write a function to check if a string is a palindrome or can be rearranged to form a palindrome.

Medium · data structures and algorithms

Approach

  1. Name the brute-force solution and its complexity before improving on it.
  2. Choose the data structure from the access pattern, not from familiarity.
  3. Restate the input: its shape, its size, and what is guaranteed about it.

Follow-up

  • How does this change if the input no longer fits in memory?
  • What is the worst case, and how likely is it on real data?

Given an array of stock prices over time, calculate the maximum profit you can achieve by buying and selling a single stock.

Medium · data structures and algorithms

Approach

  1. Walk one small example through your approach before writing the whole thing.
  2. State the target complexity and say which constraint rules the naive version out.
  3. Restate the input: its shape, its size, and what is guaranteed about it.

Follow-up

  • How does this change if the input no longer fits in memory?
  • Which test case would catch an off-by-one here?

Compare the structural and performance differences between HashMaps, HashTables, and LinkedLists.

Medium · languages, concurrency and fundamentals

Approach

  1. Say what the runtime actually does before reasoning about the code.
  2. Identify the window where an invariant is briefly untrue.
  3. Reach for the cheapest primitive that closes the race, not the broadest lock.

Follow-up

  • What happens if two callers reach this at the same time?
  • Where could this allocate more than you expect?

Explain the key differences between abstract classes and interfaces in Java, and when you would choose one over the other.

Medium · languages, concurrency and fundamentals

Approach

  1. Identify the window where an invariant is briefly untrue.
  2. Reach for the cheapest primitive that closes the race, not the broadest lock.
  3. Name what is shared across threads and what owns each piece of state.

Follow-up

  • Where could this allocate more than you expect?
  • How would you prove the race exists rather than suspect it?

Peak distinct unit types requested in a sliding minute

Easy · sliding window · two pointers · distinct count · abuse detection

An append-only search log for one client gives events (ts_ms, unit_type_id) with non-decreasing ts_ms, up to 5,000,000 events. Automated scraping is characterised by breadth rather than volume: a person reloads one property, a crawler walks a catalogue. Return the maximum number of distinct unit_type_id values this client requested inside any 60,000 ms window under the half-open convention [t, t + 60000), and the window start at which that maximum is first reached. Target O(N) time, with space proportional to the distinct values alive in one window rather than to N.

Approach

  1. Two indices over the array. Advance right, increment count[unit_type_id], and increment a separate distinct counter only on the 0 -> 1 transition.
  2. Shrink from the left while ts[right] - ts[left] >= 60000: decrement the count, decrement distinct on the 1 -> 0 transition, and erase the key. The half-open convention makes the condition >= rather than >; with > the window spans 60,000 ms inclusive and every reported peak drifts by the events sitting exactly on the boundary.
  3. Track the running maximum of distinct and the ts[left] at which it was first reached, updating only on a strict increase so ties resolve to the earliest window.
  4. Each index is admitted and evicted exactly once, so the total work is O(N) amortized even though the eviction is a while loop inside the scan. Erasing at count zero is what keeps space at window-distinct rather than session-distinct; leaving zero-count keys behind turns the map into an O(N) structure over a long session.
  5. Trade-off: this requires the client's events in timestamp order in one place. If they arrive out of order across ingest partitions, two pointers are invalid — you need a watermark plus a reorder buffer sized to the expected skew, which raises the memory bound from window-distinct to skew-distinct and adds the watermark delay to detection latency.

Worked solution (20 min)

  1. Take seven events: (0, A), (10000, B), (20000, A), (30000, C), (59999, D), (60000, E), (61000, A).
  2. Walk right from 0 to 6, maintaining count, distinct and the left pointer, and write the window contents at each step.
  3. Note the one eviction: at right = 5, 60000 - 0 >= 60000, so left advances from 0 to 1 and A's count drops from 2 to 1 without changing distinct.
  4. Record the maximum distinct value and the ts[left] at the step where it is first reached.
  5. Separately compute the maximum event count over the same windows and compare the two answers.

Expected result: Maximum distinct is 5, first reached at right = 5 with the window anchored at ts = 10000 covering events 1 through 5 (B, A, C, D, E). The maximum event count over the same windows is 6, at indices 1 through 6.

Verify it yourself

  • Distinct 5 and event count 6 must differ; if they match, the 0 -> 1 and 1 -> 0 transition guards are not being applied.
  • At right = 4, 59999 - 0 = 59999 < 60000, so left must still be 0 and distinct must be 4. A left pointer that has already moved means the shrink condition is > instead of >=.
  • The reported window start is 10000, not 60000 — the answer is the left edge of the window, not the timestamp of the event that triggered the peak.
  • The map holds at most 5 keys at any point; a larger map means zero-count keys are not being erased.

Follow-up

  • Make it streaming with bounded memory and approximate distinctness. What does HyperLogLog cost you in accuracy at the thresholds you would actually alert on, and can you still evict from the left?
  • The same client_id is shared by a corporate NAT. What second signal would you combine with breadth before acting, and why is breadth alone not enough?
  • How does the answer change if you need the peak over all clients simultaneously rather than one at a time?

Decide whether stay-range availability is derived or precomputed

Hard · denormalisation · read amplification · cache invalidation

Searches outnumber bookings by two to three orders of magnitude. One result page with plus or minus three days of flexibility expands to roughly 10^3 (unit type, range) evaluations over 10^3 to 10^4 rows of ari_daily(unit_type_id, stay_date, remaining_units, restrictions), against a whole-page p99 near one second. Decide whether bookability for a length of stay is derived from the unit-date rows at request time or materialised. Size both options with real numbers over a 450-day horizon and a 30-night maximum stay, and specify the invalidation path for a single supplier push.

Approach

  1. Size the materialised option before arguing about it. A row per (unit type, start date, length of stay) over a 450-day horizon with stays up to 30 nights is about 13,500 rows per unit type against 450, a thirty-fold increase in a table the ingest path already rewrites millions of times a day. The killer is not the build but the invalidation: a change to one night invalidates every range covering it, which is the sum of one through thirty, or 465 entries per unit type per changed night.
  2. Size the derived option honestly too. A thousand candidates times up to seven date offsets times four nights is on the order of 10^4 narrow rows per page, served by short index scans on the primary key — a few megabytes of buffer traffic and tens of milliseconds warm. That fits the budget only while those rows are resident, so the real question is the working set and its eviction behaviour, not the row count.
  3. Choose the grain that composes. Cache at (unit type, stay date), bounded by unit types times horizon, and compose ranges in memory at request time: every entry is reused by every length of stay and every flexibility offset that touches that night, and one supplier push invalidates exactly the unit-dates it wrote, one entry each, with no range expansion at all.
  4. Reject the request-shaped cache explicitly. Keying on destination, dates, length of stay, party composition, currency, point of sale and promotion eligibility is a cartesian product whose hit rate is near zero outside the head of the distribution, and any input omitted from the key serves one traveller another traveller's price — a class of defect customers report before monitoring notices it.
  5. Permit one denormalisation for pruning only, and make it conservative. A coarse per-(unit type, month) has-any-availability summary may over-report but must never under-report, used to skip candidates before the exact evaluation runs. A false positive costs one wasted range scan; a false negative hides bookable inventory and is invisible in every metric you currently have.
  6. State the cost of stale in the currency that matters. Availability served a few minutes old converts into supplier rejections at confirm time, after the card has been authorised, so the freshness target follows from the rejection rate you will tolerate. The measurement that settles the design is rejection rate plotted against the age of the availability behind each offer, not cache hit rate.

Worked solution (40 min)

  1. Compute rows per unit type for both designs over 450 days and stays of one to thirty nights, and the invalidation count for a single night's change in each.
  2. Measure the derived path end to end with a warm cache and a cold one, recording buffer counts from the analysed plan.
  3. Replay one day of real search shapes against a per-night cache and against a request-key cache and compare hit rates on identical traffic.
  4. Plot confirm-time rejection rate against the age of the availability that produced each offer.

Expected result: A written decision to derive from per-night rows behind a per-(unit type, stay date) cache, supported by the 465-entry invalidation figure for the materialised alternative and a hit-rate comparison showing the request-key cache near zero outside the head of the distribution.

Verify it yourself

  • The invalidation arithmetic is reproduced from a fixture rather than asserted.
  • The per-night cache's hit rate is far above the request-key cache's on the same traffic.
  • Cold-cache p99 for one page stays inside the budget once the fan-out is bounded.
  • Rejection rate rises measurably with availability age, which turns the freshness target into a number.

Follow-up

  • Denormalising listing_status onto ari_daily would make the availability scan index-only. What does delisting one unit type cost then, and whose writes does it contend with?
  • A popular date falls out of cache during a flash sale. How do you stop two thousand concurrent searches all recomputing it at once?
  • How would you measure whether the per-night cache is genuinely reused across lengths of stay rather than filled once per request and discarded?

Reconcile night revenue against captures without fanning either out

Medium · join fan-out · conditional aggregation · grain mismatch

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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?

How would you design a distributed web crawler and tagging system to process travel reviews across millions of POIs (Points of Interest)?

Medium · system design and architecture

Approach

  1. Name the failure you are designing for, then the recovery path.
  2. Choose a partition key and say what query it makes expensive.
  3. State the consistency you need, and where you are willing to be stale.

Follow-up

  • How does this behave when that dependency is down for an hour?
  • What breaks first when traffic grows ten times?

Design a scalable rate limiter to protect backend APIs from excessive traffic spikes and denial-of-service attempts.

Medium · system design and architecture

Approach

  1. Choose a partition key and say what query it makes expensive.
  2. State the consistency you need, and where you are willing to be stale.
  3. Name the failure you are designing for, then the recovery path.

Follow-up

  • What would you drop to keep the system up under load?
  • How does this behave when that dependency is down for an hour?

Debug a snippet of asynchronous JavaScript code containing race conditions and unexpected state mutations.

Medium

Approach

  1. Say what you would check first and why it is the highest-information step.
  2. Clarify what is being asked and what a complete answer contains.
  3. Work from the requirement backwards to the design.

Follow-up

  • How would you know your answer was wrong?
  • What assumption would you test first?

Cache the rate calendar at the unit-night grain

Easy · read-heavy · caching · invalidation · cache keys

Availability reads run at 40,000/s. ari_daily holds one row per (unit_type_id, stay_date) across a 450-day horizon for 500,000 unit types, and ari-ingest rewrites parts of it continuously. A colleague proposes caching on (property, check-in, nights, party, currency). Design the cache at the (unit_type_id, stay_date) grain instead: state the memory footprint, how a length-of-stay range is composed and restriction-checked from cached rows, exactly what one supplier push invalidates, and how you stop a stampede on a popular date.

Approach

  1. Size both key spaces before choosing. The request grain is the product of destination, check-in date (450), length of stay (1-28), party composition, currency and point of sale, so entries are barely reused outside a handful of routes and the cache pays for itself only in memory. Worse, any input omitted from that key serves one traveller another traveller's price, and that class of bug is found by customers rather than by monitoring.
  2. Size the night grain: 500,000 unit types times 450 dates is 2.25 x 10^8 entries. Packed payload is about 24 bytes (price as int64 minor units, remaining_units int16, a restriction bitfield, min/max LOS, the supplier sequence), so payload alone is roughly 5 GB and realistic per-entry key and store overhead puts the working set in the low tens of gigabytes: a sharded memory tier, not one node.
  3. Compose ranges at request time. For a stay [check_in, check_out) of n nights, read n+1 rows, check_in through check_out inclusive, because closed_to_departure is evaluated on the checkout date and the checkout date is not itself a booked night. The stay is bookable when every night in [check_in, check_out) has remaining_units >= units and stop_sell false, the arrival night is not closed_to_arrival and satisfies min_los and max_los, and the checkout date is not closed_to_departure. That is O(n) in-memory comparisons with n at most about 30, so a plus-or-minus-three-day search over three unit types is on the order of 10^2 compositions over a few hundred cached rows.
  4. Invalidate per unit-date by writing through from ari-ingest: one push touching (u, d) overwrites exactly one key. Under the request-grain cache that same push invalidates every cached range covering d, which is every arrival date within max_los of it crossed with every length, on the order of 10^2 to 10^3 keys you would have to enumerate or else knowingly serve stale.
  5. Handle the hot date: single-flight per key so a miss produces one loader and every other caller waits on the same result, cache negative lookups so a genuinely absent row does not hit the database on every request, and keep a short jittered TTL only as a backstop behind write-through rather than as the freshness mechanism.
  6. State the boundary: the cache holds inventory facts per night and never a composed price. Taxes, fees, promotions and FX are quote-time composition in pricing-and-offer, which is precisely what keeps party composition and currency out of the cache key.

Worked solution (20 min)

  1. Load ari_daily with 50,000 unit types across 450 dates (22.5M rows) and populate a cache process with the packed per-night representation; divide resident memory by entry count to get real bytes per entry.
  2. Implement compose(check_in, nights, units) over n+1 cached rows, applying each restriction at its correct night, with fixtures for a range whose middle night is closed_to_arrival and a range whose checkout date is closed_to_departure.
  3. Replay an hour of search traffic against both cache designs and record hit rate and resident bytes for each.
  4. Push one ARI update for a single (unit_type_id, stay_date) and count the keys each design must invalidate.

Expected result: Measured bytes per entry land in the tens, so the full horizon for 500,000 unit types is a low tens of gigabytes across a sharded tier. On identical traffic the night-grain cache reaches a hit rate an order of magnitude above the request-grain cache, because every flexible-date and length variant reuses the same night rows. The range with a closed_to_arrival middle night is bookable; the range whose checkout date is closed_to_departure is not. The single push invalidates exactly one night-grain key and on the order of a hundred request-grain keys.

Verify it yourself

  • Resident bytes divided by entries is within a factor of two of the paper estimate; a larger gap means the store's per-entry overhead, not your payload, is the thing you mis-sized.
  • For a sample of ranges, cached composition and a direct ari_daily query agree on both bookability and per-night price.
  • No cache key contains party composition, currency or point of sale.

Follow-up

  • Ingest applies a full refresh for one unit type's entire horizon. What does the cache do, and how do you stop 450 invalidations from becoming 450 round trips?
  • The node holding a hot city's unit types dies at peak. What does search return over the next 30 seconds, and what does it tell the caller?
  • How does a reader distinguish a cached row that is stale from one that is merely old?

Search latency doubles after a tax lookup lands

Easy · n+1 · connection pool · latency regression

Search p99 went from 620ms to 1.4s after a release. No statement in the slow-query log exceeds 4ms, database CPU is up 40%, and the connection pool now reports acquire wait time. The release moved per-jurisdiction tax rates out of the in-memory ruleset and into a lookup performed during pricing. A 30-result page prices three unit types per property over a three-night stay. Deliverable: an ordered diagnostic checklist, the arithmetic for the per-request statement count, and the fix.

Approach

  1. Establish that the regression is in statement count, not statement duration: diff pg_stat_statements calls per request between the two releases, or count spans per trace. A flat mean duration beside a large calls delta is the N+1 signature, and the slow-query log cannot show it by construction because no individual statement is slow.
  2. Multiply the fan-out out loud: 30 results x 3 unit types x 3 nights = 270 lookups per page, at least one per night per jurisdiction. At 200 requests/second that is roughly 54,000 statements/second arriving at a pool of a few dozen connections.
  3. Separate service time from queueing: compare pool acquire time against mean statement time. Latency rising while statement duration is unchanged means the pool is the constraint, and wait time grows sharply as utilisation approaches saturation rather than proportionally to load.
  4. Fix at the right level: restore the pure, versioned in-memory ruleset, since the offer service is specified to do no per-call I/O and a pinned ruleset version is what makes a quote replayable in a dispute months later. If a read is genuinely unavoidable, issue one batched query per request keyed by (jurisdiction, ruleset_version) -- O(1) round trips instead of O(results x nights).
  5. Add the guard that catches the next one: assert a per-request statement-count ceiling in an integration test, so a reintroduced lookup fails CI rather than p99.

Follow-up

  • With the ruleset in memory, what happens when tax rules change mid-flight, and what does the quote pin so a capture or refund months later reconciles against the amount the traveller agreed to?
  • Pool acquire time is now the bottleneck metric. What do you measure before raising pool size, and what breaks when the fleet's total pool exceeds the database's connection budget?

Suggested practice checklist

For a candidate senior enough that the loop turns on design and judgement rather than on whether the coding round gets finished. Five days build one system properly and then stress it; coding gets a single maintenance day, on the assumption that the risk at this level is an unexamined tradeoff rather than a missed algorithm.

Day 1 — Numbers before diagrams

  • Build your own reference card of the figures you will re-derive all week: bytes for a realistic record, requests per second implied by a given daily active count, and the storage that a year at a given write rate produces. Derive each one rather than copying it, because the derivation is what survives a follow-up.
  • Turn one product statement into capacity requirements. From ten million daily users at four writes and forty reads each, state the peak-to-average factor you are assuming and why, then produce peak write QPS, peak read QPS and a year of storage.
  • Write the two numbers whose order of magnitude changes the design, the read-to-write ratio and the working-set size against memory per node, and state the threshold at which each one flips your answer.

Deliverable: A one-page numbers card and one worked capacity estimate with every assumption written down.

Day 2 — One system, from requirements to schema

  • Spend the first ten minutes producing only functional requirements, non-functional targets with numbers attached, a p99 latency, a durability expectation, a consistency requirement, and an explicit out-of-scope list.
  • Define the interface before the boxes: the three or four endpoints, their parameters, what each returns, and which of them are idempotent.
  • Write the data model, then write the single access pattern that justifies it, and state what the schema would have to become if the dominant access pattern were the other one.

Deliverable: One design carried to endpoint-and-schema depth, with non-functional targets expressed as numbers and a written out-of-scope list.

Day 3 — The consistency you are actually buying

  • Write out what a client sees under asynchronous replication when its write commits on the leader and its next read is served by a lagging follower, then write the two fixes, pinning that session's reads to the leader for a bounded window or carrying a version token the replica must reach, and the cost of each.
  • Work the quorum arithmetic on paper for N of three with W and R of two, and separate what R + W > N does guarantee, that any read set intersects any write set, from what it does not: on its own it is not linearizability, and a sloppy quorum that accepts writes on nodes outside the preference list breaks even the intersection.
  • Take two storage choices with different defaults, a single-leader relational store committing synchronously and a quorum-replicated store that converges eventually, and write the specific product behaviour that would be wrong under each, rather than a general statement about which is stronger.

Deliverable: A page separating what quorum overlap guarantees from what it does not, with one concrete product misbehaviour attached to each gap.

Day 4 — Failure is the design

  • For one write path, work through the case where the client times out after the server has already committed, then design the idempotency key: who generates it, how long it is retained, and what the duplicate request returns.
  • Express the retry policy as parameters rather than as a word: maximum attempts, base delay, backoff factor, jitter, and which error classes are retried at all. Then state why retrying a non-idempotent write without a key is a correctness bug and not merely waste.
  • Compute the fan-out effect on tail latency. If a request waits on ten backends and each independently exceeds its p99 one percent of the time, the chance at least one is slow is 1 - 0.99^10, about ten percent. Then write why independence is the optimistic assumption and what correlates them in practice.
  • Name the backpressure mechanism for one queue or one dependency in the design, a bounded queue with shedding or a concurrency limit, and write what the caller is told when it engages.

Deliverable: One write path with an idempotency design, a parameterised retry policy, and a written tail-latency calculation with its assumption named.

Day 5 — Scaling the hot path

  • Choose cache-aside or write-through for one read path and write the staleness window each produces, then name the invalidation event and what the system does when that event is lost.
  • Design against the stampede: either coalesce requests so only one recomputes a missing key, or refresh early with jittered expiry, and write why identical TTLs on keys populated in the same moment produce a synchronised expiry and a thundering herd.
  • Shard one table by a key you choose, then answer the two questions that break the choice: which queries now require a scatter-gather, and what happens to the distribution when one tenant is a hundred times larger than the median.
  • Write the cost of adding a node under plain modulo placement, where nearly every key moves, against consistent hashing, where roughly one key in n+1 moves, and state what virtual nodes are for.

Deliverable: A caching and sharding decision for one path, each with its failure mode and its rebalancing cost written beside it.

Day 6 — Keep the coding hand in, at the bar that applies to you

  • Solve one medium problem in thirty minutes, then spend twenty more making it production-shaped: named invariants, validation at the boundary, and errors that distinguish a caller mistake from an internal fault.
  • Write the tests you would require of a colleague's version of that function: one for empty input, one for the boundary, and one for the case the implementation is most likely to get wrong.
  • Read a piece of your own code from six months ago and write the change you would ask for, phrased as you would actually phrase it in review.

Deliverable: One problem hardened to review standard, with its test list and one written review comment.

Day 7 — Defend it while being interrupted

  • Run a forty-five-minute design mock with an interviewer briefed to change a requirement halfway, a tenfold traffic increase or a new strict consistency requirement, and to push on one number you estimated.
  • Rehearse the two sentences a senior loop is listening for: naming the tradeoff you are choosing against and why, and saying what you would measure to learn that the choice was wrong.
  • Prepare the design you regret: a real decision, the constraint that produced it, what it cost, and what you changed afterwards.

Deliverable: Mock notes recording how the design changed under the new requirement, plus a written account of one regretted decision.

Review and mentoring: what you let through

Counting review comments or mentees proves nothing. The useful version is a specific change you approved with a reservation you stated, or one you blocked and the delay that cost. Say which standard you were holding and why it was worth the friction. A mentoring story needs the thing the other person can now do without you.

Give an example of how you mentored a junior engineer or drove engineering standards within your previous team.

Medium · behavioural and engineering judgement

Approach

  1. Give the blast radius: what could have broken, and what you measured.
  2. Pick a story where you made the decision, not one where you watched it.
  3. 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?

Estimate an ingest rewrite you have never attempted

Hard · estimation · ingest · write amplification · migration

You are asked to estimate replacing the ARI ingest path so that a supplier's full refresh applies as a set difference over the affected horizon instead of row-by-row upserts, across roughly 450 forward days for hundreds of thousands of unit types, with no degradation of the search read path during the change. You have never built this. Give the estimate, the decomposition behind it, the three unknowns that dominate the range, the spike you would run first, and the condition under which you would come back and re-estimate.

Approach

  1. Decompose into shippable pieces before producing any number: refresh semantics, the write path itself, a dual-run comparison against the current path, per-supplier cutover, and rollback. Estimating the whole as one figure is the failure mode; estimating five pieces gives you somewhere to put the uncertainty.
  2. Name the semantic unknown first, because it dominates and it is not an engineering problem. A set difference needs the affected horizon, and suppliers differ in whether a full refresh declares its horizon or leaves you to infer it. If you must infer, the correct behaviour on an ambiguous message is a decision with a revenue consequence: withdraw too much and you stop selling live dates, withdraw too little and you keep selling dates the supplier has pulled.
  3. Quantify the write-amplification unknown rather than describing it. One refresh rewriting a 450-day horizon for a large supplier is millions of unit-date writes in minutes against the hottest table in the system; in PostgreSQL every update writes a new row version, so this produces dead tuples and vacuum pressure, and HOT updates only avoid index churn when no indexed column changes and the page has room. Whether the read path survives that is measurable, not arguable.
  4. State the third unknown as the one that is usually discovered late: how many suppliers actually send full refreshes, how often, and how large, because the cutover cost is per supplier and the tail of odd behaviour is where the schedule goes.
  5. Design the spike to collapse the widest unknown in the least time: replay the largest historical refresh you have against a copy of the table and measure rows touched, wall clock, dead tuples produced and the search path's p99 during the replay. Two days of that is worth more than a week of design.
  6. Give the estimate as a range with the multiplier attached to a named unknown, plus a re-estimate trigger such as the replay exceeding a stated p99 impact. A single number with no stated dominating unknown is the answer that gets you held to it.

Follow-up

  • Halfway in, the replay shows the read path degrading badly. What do you change, and does the estimate move?
  • How would you run both paths in parallel and prove they agree, without doubling the write load on the hot table?
  • Someone needs a date to give a partner. What do you commit to, and what do you explicitly not commit to?

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

  1. 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'.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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?