cswork / libraryOpen practice workspace

Interview guides · AIDA Cruises

AIDA Cruises Software Engineer Interview Questions & Guide 2026

System design practice for AIDA Cruises software engineer candidates: reach a data model and an API contract, not a box diagram.

The 60-second brief

At AIDA Cruises, a Software Engineer occupies a pivotal role in maintaining the technical excellence that powers the world’s leading cruise line. You are not just writing code; you are building and managing the digital infrastructure that ensures seamless operations, guest satisfaction, and robust CRM solutions. Your work affects the daily experience of both AIDA Cruises' crew and its passengers, bridging complex technical systems and high-stakes hospitality.

Prepare in one language you know well enough to debug in rather than the one you think reads best. Under a clock an unfamiliar language costs you standard-library lookups and iteration mechanics, and that time comes out of your thinking budget, not your typing budget.

Focus areas

  • Derive idempotency keys from attempts, never from retries
  • Decrement per-night inventory with one conditional update
  • Hold money in minor units, rounding per jurisdiction

Overview

This role requires a unique blend of technical precision and operational awareness. Whether you are managing CRM applications or optimizing software reliability, you will be expected to demonstrate a deep understanding of how your solutions serve the business. It is a challenging, fast-paced environment where the ability to remain calm under pressure—often while troubleshooting real-time technical hurdles—is just as critical as your programming proficiency.

The AIDA Cruises interview process

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

Initial Screening

The title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.

What to demonstrate

  • Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
  • Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
  • Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year

How to prepare

  • Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
  • Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
  • Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter

Interactive Stages

You cannot drill a format you do not know, so put the preparation into material that travels. Three pieces of your own work, each rehearsed until you can take a follow-up you did not anticipate, will carry a conversation or a code walkthrough equally well. Specificity is what separates that from filler. A number needs its definition before it means anything: a p99 is over some window and measured at some hop, and a server-side figure excludes the queueing and network time a client would see. The number you cannot qualify is the one to leave out.

What to demonstrate

  • Whether your examples carry detail only someone who did the work would hold, such as what the binding constraint actually was, which alternative you rejected and why it was worse, and what you measured on each side of the change
  • Whether a number survives one follow-up, meaning you can say what it was measured over and whether it moved because of your change or merely alongside it
  • Whether a failure is described with the specific change that followed it, rather than a lesson stated in general terms
  • Whether your part in a team effort is stated accurately, including what other people did

How to prepare

  • Write a page on each of three projects covering the constraint, the option you rejected, the measurement before and after, and what went wrong. Cut any line you cannot take a follow-up on, since you are writing the parts you will be pressed on rather than a summary.
  • Recover the real figures while you still have access: request volume, data size, latency with its percentile and window, team size, timeline. Note where each came from, whether a dashboard, a design document or memory, and mark the estimates so you can say which they are out loud.
  • Take your weakest project story to someone who works in a different area and have them ask why four times in succession. The point where you run out of answer is the part to go and re-read before the round.

Assessment Center

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.

Final Technical Evaluation

Coding rounds mostly set a floor. They decide whether you clear the bar, not where you land on the ladder. Level tends to come out of the design discussion and the ownership stories, so the question worth auditing beforehand is whether the scope you describe matches the scope of the job. Work that stops at your own service, or a story whose hard part was writing the code rather than getting several people to agree on an interface, reads a level below where you think you are interviewing, and that gap is usually resolved downwards.

What to demonstrate

  • Whether the largest thing you describe owning ran end to end — the decision, the migration path, the rollout, and what you did when it went wrong — or stopped at the change you merged
  • Whether design answers include what you would not build, what you would defer, and what you would measure before committing, rather than only what the boxes are
  • Whether a disagreement in a story was settled with something checkable — a benchmark, a prototype, a written proposal — instead of by seniority or by waiting it out
  • Whether you can say which calls you made alone and which you escalated, and why the line sat where it did

How to prepare

  • Write your largest piece of owned work as a timeline of decisions — who decided what, when, and what you did when the plan broke — then delete every sentence whose subject is "we" and see how much survives
  • Take one system you know well and drill the migration answer: how old and new paths run side by side under live traffic, how you compare their outputs, what the rollback is once writes are going to both, and which step you would not automate
  • Map each line of the ladder in the job posting to a specific thing you have done, find the line you cannot support, and prepare the closest evidence you have plus an honest account of the gap

Where candidates lose points

Representing money as a float, and rounding once at the end

Currencies have different ISO 4217 exponents, so the reflex of multiplying by 100 is wrong for zero-decimal currencies such as JPY and KRW and for three-decimal currencies such as KWD, BHD and OMR, and it is wrong in a direction that gets worse with volume. Separately, taxes and fees are levied per night and per jurisdiction and each rounds on its own, so the sum of rounded per-night amounts is not the rounded sum, and the two differ by a few minor units on a typical stay. Neither bug shows up in testing with a single currency and a single-night booking. Both show up as a reconciliation mismatch against the supplier or the processor, at which point somebody has to explain a line-item discrepancy across thousands of bookings without a per-night audit trail to do it with.

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.

Sharing mutable state with no stated owner

Say which thread, request or task owns each mutable structure, and what protects it when the answer is more than one: a lock, a queue that hands ownership across, or an immutable copy per reader. A structure documented as safe for concurrent reads is usually not safe for a concurrent write alongside those reads.

Retrying a write that is not safe to repeat

A timeout tells you nothing about whether the server applied the write, so a blind retry of a create or a charge can duplicate it. Either make the operation idempotent, with a caller-supplied key the server deduplicates on or a conditional update, or do not retry it; and use exponential backoff with jitter so the retries of many clients do not synchronise into a second outage.

Questions & practice

Order saga compensations over the steps that actually ran

Medium · topological sort · dag · saga · compensation

A booking saga is a DAG of at most 64 steps with at most 200 edges. Each step carries a compensating action, and some are flagged not cleanly compensable. At runtime every step is in one of three states: completed, unknown (the call timed out and the outcome is not known), or not_started. Produce a valid forward execution order, detect a cycle in the step graph, and on failure produce the order in which compensations must run over the steps that actually ran. Target O(V + E). State explicitly what your compensation order does with an unknown step.

Approach

  1. Kahn's algorithm for the forward order: compute in-degrees, seed a queue with the zero-in-degree steps, pop and decrement. O(V + E), and with V <= 64 an in-degree array plus an adjacency list is the whole data structure.
  2. Cycle detection falls out of the same pass. If fewer than V nodes are emitted, the residual nodes lie on or downstream of a cycle — report that set rather than a boolean, because the residual is the diagnosis.
  3. For compensation, build the induced subgraph over the steps in completed or unknown, topologically sort that subgraph, and reverse it. Reversing the planned order instead is the bug: it schedules compensations for steps that never ran, and a bare remaining_units = remaining_units + n release applied against a hold that was never taken hands out capacity that does not exist.
  4. An unknown step is compensated, never skipped. Its compensation begins with a read-back against the stored idempotency key — resolve what actually happened, then cancel it if it happened. A design that treats a timeout as a failure and skips the compensation is how a supplier reservation survives a booking that failed.
  5. Surface the not-cleanly-compensable step at design time rather than at runtime. A supplier confirm against an API with no read-back cannot be resolved automatically, which forces a manual reconciliation queue and changes the operational scope of the feature, so it is a thing to raise in the first five minutes.
  6. Trade-off: reverse topological order is a partial order, so independent compensations can run concurrently and shorten the window in which money is held without inventory. The price is that a partial compensation failure leaves a mixed state, so each compensation still has to be individually idempotent before you are allowed to parallelise.

Follow-up

  • Two compensations are independent in the DAG but contend on the same unit-date rows. Does the partial order still hold, and what do you add?
  • A compensation itself times out. What state does that step enter, and how does the reconciler tell it apart from one that never started?
  • Where does the transactional outbox sit in this graph, and what does a duplicate delivery do to each consumer?

Price every flexible date range without rescanning nights

Hard · prefix sums · incremental window · read amplification · latency budget

One unit type has cached per-night rows over a 43-day slice, each with price_minor, remaining_units, closed_to_arrival, closed_to_departure, stop_sell and min_los. A flexible search asks for 15 candidate check-in dates crossed with lengths of stay 1 through 28, for a party needing one unit, across 5,000 candidate unit types, inside a 300 ms budget. For each length of stay, return the cheapest legal check-in date and its total price. Rescanning the nights of every range is correct; say why it does not fit the budget, and give an approach that does.

Approach

  1. Quantify the naive cost before optimising it. Rescanning touches the sum of L for L = 1..28, which is 406 nights per check-in, so 15 x 406 = 6,090 night touches per unit type and about 3.0 x 10^7 per request across 5,000 candidates. At the tens of millions of simple operations per second a managed runtime delivers on one core, that is most or all of a 300 ms budget before pricing, ranking and serialisation are paid for.
  2. Exploit the incremental structure: for a fixed check-in, extending the length of stay by one night adds exactly one night to the running price sum, one candidate to the running minimum of remaining_units, and one term to the running OR of stop_sell. Sweep check-in on the outer loop and length of stay on the inner, carrying those three accumulators.
  3. Each (check-in, length) evaluation is then O(1), giving 15 x 28 = 420 night touches per unit type and about 2.1 x 10^6 per request — roughly 14.5x less work — with O(1) extra space and no preprocessing pass at all.
  4. Apply every restriction at its own night, because their scopes genuinely differ. closed_to_arrival, min_los and max_los are read from the arrival row only. stop_sell is the OR over the booked nights [i, i+L). closed_to_departure is read from the row at index i + L, which is the checkout date and is not a booked night; if that row falls past the end of the cached slice, the range is unevaluable and must be excluded rather than assumed open.
  5. If the access pattern were arbitrary (i, j) pairs in random order rather than a structured sweep, the incremental trick dies and the right structures are a prefix sum for price (O(D) build, O(1) query) and a sparse table for the range minimum of remaining_units (O(D log D) build, O(1) query). The sweep beats both here only because the query order is under your control.
  6. The remaining lever is not algorithmic. The 5,000 candidate unit types dominate the cost, so prune before this loop — by destination geometry, by a per-night price floor, or by taking only the top-ranked candidates — and return partial results with an explicit completeness flag when the budget expires anyway.

Follow-up

  • The party needs two units rather than one. What changes in the accumulators, and does any restriction scope change with it?
  • Rank by total price per night instead of total price. Can you prune across lengths of stay now, and what makes that valid?
  • The cached slice is per (unit_type, stay_date). What does one supplier push invalidate, and how do you stop a popular date from stampeding the recompute?

Merge k sorted supplier streams under a deadline budget

Medium · heap · k-way merge · top-k · deadline budget

A search fan-out returns K <= 40 supplier result streams, each already sorted ascending by total_minor converted to the display currency using the quote's pinned fx_rate_id. Streams are iterators and advancing one costs a network read. Up to 10,000 results exist across all streams; the page needs the k = 30 cheapest. The request carries a remaining deadline budget, and a stream that misses it is dropped. Return the k cheapest offers plus a completeness flag, minimising elements read. State the cost in heap operations and in stream reads, and state what the flag guarantees.

Approach

  1. Seed a min-heap with one head element per stream: K entries, heapified in O(K). Do not materialise all 10,000 results — a full merge is O(N log K) with N = 10^4 to produce 30 answers, which is more than two orders of magnitude of wasted network reads.
  2. Pop the minimum, emit it, then advance only the stream it came from and push its next element. After k pops you hold the k globally cheapest. Elements read is bounded by K + k - 1 = 69, and heap work is k pops at log K, roughly 30 x 5.3 comparisons. Skipping the refill on the final pop is what saves the last read.
  3. Derive each stream read's timeout from the budget remaining at the instant the read is issued, not from a fixed constant. A fixed per-call timeout lets the last connector spend a budget that earlier connectors already consumed, and the request blows through its deadline while every individual call looks compliant.
  4. When a stream is dropped, remove it from the heap and set the completeness flag. State the guarantee precisely: the k-th price returned is an upper bound on the true k-th price, because a dropped stream could only have contributed something cheaper. That is a claim a caller can act on; a bare boolean flag is not.
  5. The sortedness precondition is load-bearing. If the connector contract does not promise it, the shape changes to a bounded max-heap of size k over every result, O(N log k) with no early exit and every element read. Which regime applies is a property of the connector, worth pinning before writing either version.
  6. Comparison must happen on one currency at one pinned FX rate. Comparing price_minor across currencies orders offers by the accident of their ISO 4217 exponent, and re-fetching a live rate mid-merge makes the ordering non-reproducible in a later dispute.

Worked solution (20 min)

  1. Take four streams: S1 = [12000, 18000, 25000], S2 = [9000, 21000], S3 = [30000], S4 which times out before returning anything.
  2. Seed the heap from the three responsive heads and record the reads so far.
  3. Pop three times, refilling from the popped element's stream after the first two pops and stopping before refilling on the third.
  4. Tally elements read and the maximum heap size, and check them against the K + k - 1 bound with K = 3 responsive streams and k = 3.
  5. State the completeness flag and the bound it implies against S4's unknown contents.

Expected result: Results are 9000, 12000, 18000 in that order. Completeness flag is false. Five elements are read (three heads plus 21000 and 18000); 25000 is never read. Heap size never exceeds 3, matching K + k - 1 = 5 reads.

Verify it yourself

  • 25000 must never be read. If it is, the implementation refills after the final pop and is paying one avoidable network read per request.
  • The heap holds at most one element per stream at any moment; a heap larger than K means whole streams are being pushed.
  • The returned third price, 18000, is an upper bound on the true third price had S4 answered — verify by adding a 10000 result to S4 and confirming the true answer becomes cheaper, never more expensive.
  • Reordering the streams in the input changes nothing about the three returned values, only about heap tie-breaking.

Follow-up

  • One stream is consistently slowest but also consistently cheapest. How do you stop the deadline from systematically excluding the best prices?
  • Results within a stream are sorted by price, but the page ranks by a blended score. What breaks, and what do you need from the connector to fix it?
  • Model the tail: if each of 20 connectors independently exceeds its own p99 one percent of the time, what fraction of requests loses at least one, and does that number justify partial results?

Add a held-units column and constraint to a live calendar

Hard · online migration · lock queue · batched backfill

ari_daily holds about 225 million rows, 500,000 unit types over a 450-day horizon, and is upserted continuously by the ingest path while the search path reads it. You must add held_units SMALLINT NOT NULL DEFAULT 0, a CHECK that remaining_units - held_units >= 0, a unique index the hold path needs, and a backfill of held_units from inventory_hold_night, with no window of unavailability. Give the ordered statement plan, the lock each step takes, and how you stop one long-running query from turning a millisecond DDL into an outage.

Approach

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

  1. Clone a representative slice, add the column with a constant default, and confirm from timing and table size that no rewrite occurred.
  2. Add the CHECK as NOT VALID, insert a violating row to prove it is enforcing, then validate it and observe the lock level taken.
  3. Start a long SELECT in one session and run the ALTER in another with and without lock_timeout, watching the queue build behind it.
  4. Run the backfill in batches while the ingest path is writing, recording dead tuples and replication lag per batch.

Expected result: An ordered runbook in which no statement holds ACCESS EXCLUSIVE for more than a few milliseconds, the backfill is throttled and resumable, and the constraint is validated only once the column is fully maintained by application code.

Verify it yourself

  • Table size and timing confirm the column addition did not rewrite the table.
  • The lock view shows a share-update-exclusive lock during validation and during the concurrent index build, not an exclusive one.
  • With lock_timeout set, the blocked ALTER fails and retries instead of queueing readers behind it.
  • Killing the backfill mid-run and restarting it produces the same final held_units values.

Follow-up

  • The concurrent index build fails at eighty percent. What is on disk, what does the planner do with it, and what is your next statement?
  • A physical replica serves part of the search traffic. What does replaying this migration do to queries running there?
  • How do you make the backfill resumable after it is killed at batch four thousand of five thousand, without rescanning what it already did?

Delist a unit type without deleting its booking history

Easy · soft deletes · partial indexes · audit history

unit_type(unit_type_id, property_id, supplier_id, supplier_unit_code, listing_status in ('active','paused','stop_sell','delisted','suspended_quality'), delisted_at_utc, created_at_utc) carries UNIQUE (supplier_id, supplier_unit_code), and booking.unit_type_id references it. A supplier withdraws a unit type, then six weeks later re-publishes the same supplier_unit_code as a different room. Give the DDL that lets both rows coexist while keeping exactly one live row per supplier code, and that keeps search off the dead ones. Then write the monthly bookings-per-property query and say where the listing_status predicate goes so bookings on delisted units still appear.

Approach

  1. Rule out the hard delete twice over: the foreign key from booking has no cascade you would want, since deleting a unit type would destroy or orphan paid reservations, and the row is evidence — the rate and cancellation terms that were agreed only make sense against it. So delisted_at_utc is the instant and listing_status is the reason, and neither is optional.
  2. Move the uniqueness to the live rows only: CREATE UNIQUE INDEX ux_live_supplier_unit ON unit_type (supplier_id, supplier_unit_code) WHERE delisted_at_utc IS NULL. One live row per supplier code, unlimited dead ones, and the re-publish six weeks later inserts cleanly.
  3. Reject the three-column trick explicitly. UNIQUE (supplier_id, supplier_unit_code, delisted_at_utc) does not work in PostgreSQL, because NULLs compare as distinct and two live rows both carrying NULL are both accepted. PostgreSQL 15's UNIQUE NULLS NOT DISTINCT changes that, so it is a version precondition to state rather than assume.
  4. Make the search filter enforceable instead of remembered. A partial index on (property_id) WHERE listing_status = 'active' is usable only when the planner can prove the query predicate implies the index predicate: listing_status = 'active' does, listing_status <> 'delisted' does not, and the second form silently falls back to a sequential scan. A view over the live rows with select revoked on the base table removes the discipline problem entirely.
  5. Put the predicate in the right clause for reporting. LEFT JOIN booking to unit_type with ut.listing_status = 'active' in the WHERE clause turns the outer join into an inner join and drops every booking on a since-delisted unit from the month's revenue — precisely the rows finance will ask about. It belongs in ON, or nowhere, because the report is about bookings rather than about current sellability.
  6. Keep the two states separate. listing_status describes present sellability and paused or stop_sell are reversible; delisted_at_utc is terminal. Collapsing them into one nullable column makes a paused unit indistinguishable from a dead one to the job that decides which ARI rows are still worth maintaining.

Follow-up

  • The re-published code turns out to be the same physical room under a new name. How would you discover that, and what breaks if you merge the two rows after bookings exist against both?
  • The supplier keeps pushing ARI rows for the delisted unit. Do you keep ingesting them, and what does that answer cost in table size over a 450-day horizon?
  • Someone asks what the unit was called on the day a booking was made. Which of your tables can answer that, and which cannot?

Describe your previous experience and specific duties in your former positions.

Medium

Approach

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

Follow-up

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

Describe a situation where you had to solve a complex technical problem under pressure.

Medium

Approach

  1. State your assumptions explicitly before working the problem.
  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?

How would you react during a technical breakdown?

Medium

Approach

  1. State your assumptions explicitly before working the problem.
  2. Clarify what is being asked and what a complete answer contains.
  3. Work from the requirement backwards to the design.

Follow-up

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

How do you approach the maintenance and optimization of critical business applications?

Medium

Approach

  1. Clarify what is being asked and what a complete answer contains.
  2. Work from the requirement backwards to the design.
  3. State your assumptions explicitly before working the problem.

Follow-up

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

Bound search fan-out across twenty suppliers with one deadline

Medium · fan-out · tail latency · timeouts · circuit breaker

shopping-api must answer one trip intent within one second at p99. Thirty properties survive ranking, and twenty external supplier connectors have to be consulted for inventory you do not hold locally, each with a p99 near 300 ms and its own failure modes. Ranking and serialisation need 150 ms of that budget. Design the fan-out: how the deadline is budgeted, what per-call timeouts you issue, what the response says when a connector misses, and what a connector that has been failing for ten minutes costs the next request. Quantify the tail.

Approach

  1. Do the arithmetic first. If the 20 connectors were independent and each exceeded its own p99 one percent of the time, the probability that at least one does is 1 - 0.99^20 = 0.182. Waiting for all of them makes the page's p99 the maximum over 20 calls rather than any connector's own p99, and raising individual timeouts makes that strictly worse. The 18 percent is the optimistic case: connectors sharing an egress proxy, DNS or transit fail together, so independence is an assumption to test, not a given.
  2. Budget instead of timing out. Set a request deadline at admission, around 850 ms, reserve the 150 ms for ranking and serialisation, and derive each call's timeout from the budget remaining at the instant it is issued rather than from a constant. A call issued at t = 600 ms gets 100 ms, not 300, and below the connector's p50 it is not worth issuing at all.
  3. Return partial results with an explicit completeness flag naming which suppliers were not consulted, and make that flag part of the contract downstream. In particular, never store a partial result set under the same cache key a complete one would use, or one slow moment keeps that supplier missing for the whole TTL.
  4. Stop paying for known-dead dependencies: a per-connector circuit breaker opened on error and timeout rate over a rolling window, with a small number of half-open probes to detect recovery. An open breaker costs microseconds instead of a full timeout, which is the difference between a degraded supplier costing you one connector and costing you the page.
  5. Shrink the critical path itself. Serve everything you mirror locally from ari_daily, keep the fan-out for inventory you genuinely do not hold, and hedge only where a duplicate request is free of side effects, which means reads and never holds.
  6. Name the trade you made: an incomplete page shows fewer options and converts worse on that request, and you preferred it to a page that is complete and slow 18 percent of the time. Report incompleteness per supplier per day, or a temporary degradation quietly becomes the steady state.

Worked solution (25 min)

  1. Build 20 stub connectors with p50 90 ms, p99 300 ms and a 0.5 percent hard-failure rate.
  2. Measure page latency over 10,000 requests three ways: waiting for all, a fixed 300 ms timeout, and a timeout derived from the remaining budget.
  3. Make one connector respond only after 5 s for a ten-minute window, with and without a breaker, and record page p99 and the completeness flag.
  4. Correlate three connectors with a shared 200 ms stall once a minute and re-measure the miss rate against the independent estimate.

Expected result: Waiting for all 20 puts page p99 far above any single connector's p99, tracking the maximum order statistic. The derived budget holds page p99 under the target while dropping a small, measurable share of supplier results, all of them reported in the completeness flag. Without a breaker the 5 s connector drags every request to its timeout ceiling for the full ten minutes; with one, the impact collapses to probe traffic. Correlating three connectors raises the miss rate above 1 - 0.99^20.

Verify it yourself

  • The completeness flag is false on exactly the requests where a connector was skipped or timed out, reconciled against per-connector counters.
  • No cache entry holds a partial result set under a key a complete result would also occupy.
  • In a single request trace, successive per-call timeouts are strictly decreasing.

Follow-up

  • The connectors share one egress proxy, so their latencies correlate. Redo the tail estimate and say what changes in the design.
  • A supplier is not slow, it is wrong: it returns 200 with stale availability. Where does that surface, and how long does it take you to notice?
  • Budget remaining is 40 ms. Give the timeout, and defend issuing the call at all.

One region reports an extra night on every booking

Easy · timezones · civil dates · dst

Finance reports that for properties in one country, SUM(booking_night.night_price_minor + tax_minor + fee_minor) exceeds booking.total_minor and the night count is one greater than check_out_date - check_in_date. The booking table stores check_in_date and check_out_date as DATE; unit_type stores property_timezone as an IANA zone. The expansion that writes booking_night builds a timestamp from check_in_date, converts it to UTC, iterates adding 24 hours until it reaches the checkout instant, and casts each instant to a date. Deliverable: name every distinct defect, which zones and which dates each affects, and the correct expansion.

Approach

  1. Take one failing booking and print, side by side, check_in_date, check_out_date, every booking_night stay_date, the property's IANA zone and the process default zone. The shape of the difference identifies the defect: uniformly shifted rows are the round trip, one row past the end is the loop bound, a repeated date is the arithmetic.
  2. Name the round trip precisely. Midnight local converted to UTC and cast back to a date lands on the previous day for any zone east of UTC: local 2026-03-14T00:00 at UTC+09:00 is 2026-03-13T15:00Z. This fires every day of the year, not only at transitions, and it puts the arrival night outside the booking's own date range.
  3. Name the loop bound. Nights are the half-open interval [check_in_date, check_out_date); the checkout date is not a booked night. Iterating until the instant reaches or passes the checkout instant emits one extra row, which is the extra tax_minor and fee_minor breaking the sum identity.
  4. Name the arithmetic. Adding 24 hours is not 'the next day' in a zone that observes DST: the elapsed time between two consecutive local midnights is 23 or 25 hours across a transition, so after a transition the accumulated instants no longer sit on local midnight and the end test -- an accumulated instant compared against the checkout instant -- gains or loses a step depending on the direction of the transition. A zone that transitions at 00:00 has no local midnight on that date at all, so the start-of-day construction either throws or silently resolves to 01:00.
  5. Confirm by partitioning the mismatch: group the failing bookings by the property's UTC offset and by whether the stay range contains a transition. The round trip only moves the date for zones east of UTC -- local midnight at UTC+09:00 is 15:00Z on the previous calendar date, while local midnight at UTC-05:00 is 05:00Z on the same calendar date -- so an all-year mismatch confined to positive-offset properties is the round trip, and that is why the report names one country. The extra row from the loop bound appears in every zone; a night count that departs from that uniform off-by-one only on stays containing a transition is the arithmetic.
  6. Rewrite the expansion as calendar arithmetic over civil dates -- d = check_in_date while d < check_out_date, d = d + 1 day -- construct no instant at all, keep civil date and instant as distinct types, and backfill by recomputing booking_night rather than adjusting total_minor, since the property will bill the nights it actually served.

Follow-up

  • The cancellation deadline is 18:00 local on the day before arrival. Which branch do you take when that local time occurs twice, and what do you store so the decision can be re-derived in a dispute months later?
  • How do you find the remaining conversions of this shape in the codebase, and what fixture catches them in CI without waiting for the next transition date?

Suggested practice checklist

For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.

Day 1 — Measure before reasoning

  • Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
  • Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
  • Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.

Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.

Day 2 — References, copies, and the bugs they produce

  • Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
  • Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
  • Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.

Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.

Day 3 — Types, once, in a language that checks them

  • Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
  • Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
  • Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.

Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.

Day 4 — Concurrency, starting with what actually runs at the same time

  • Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
  • Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
  • Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.

Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.

Day 5 — Debugging as a procedure rather than an instinct

  • Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
  • Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
  • Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.

Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.

Day 6 — Tests that catch the bug you are about to write

  • Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
  • Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
  • Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.

Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.

Day 7 — Debug something broken, out loud

  • Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
  • Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
  • Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.

Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.

'We rewrote it in a better framework' is not a story

A migration is a cost you chose to pay, not an achievement. The story is what the old system made expensive, what you measured before committing, what kept serving traffic during the cutover, and what you would have done if the numbers had come back flat. Without those, a rewrite reads as taste.

Reverse your own decision on hold expiry

Medium · reversibility · hold lifecycle · measurement · judgement

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

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

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?