The Software Engineer role at American Cruise Lines represents a unique intersection of modern technical problem-solving and the specialized requirements of the maritime industry. While the title reflects core engineering functions, the role is deeply integrated into the operational reality of maintaining and optimizing the complex systems that power the American Cruise Lines fleet. You are not just writing code; you are contributing to the reliability and efficiency of vessels that provide premium travel experiences.
This position is critical to the business because it bridges the gap between software solutions and physical maritime operations. You will be expected to understand the specific rules and regulations governing life aboard American Cruise Lines vessels, ensuring that your technical contributions align with safety, compliance, and operational excellence. It is a role for those who appreciate the tangible impact of their work and thrive in an environment where technical precision meets high-stakes logistics.
Remote Interview
reportedBecause the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.
What to demonstrate
- Whether the shape of your answer matches the question, so a yes-or-no gets answered before it is justified and an open prompt gets a direction before a detour
- Whether you check how much depth is wanted instead of deciding for them, and whether you stop when the answer is complete rather than continuing until someone interrupts
- Whether you can be redirected in the middle of an answer without restarting it from the beginning
- Whether a question outside your experience gets an honest boundary followed by reasoning from what you do know, instead of a confident answer with nothing behind it
How to prepare
- Rehearse one project at three lengths, roughly thirty seconds, three minutes, and a full walkthrough at the depth of a design review, and practise switching between them when someone interrupts mid-telling
- Have someone ask you five questions of deliberately mixed type in one sitting without telling you the types, and score only whether you identified each one correctly before you started answering
- Draft the sentence you will use to check depth, along the lines of asking whether the short version is useful here or they want the detail, and use it in a real conversation this week so the day of the round is not its first outing
PracHub editorial advice for the preparation topics above.
Storing stay dates as timestamps and converting them through UTC
A check-in is a local civil date at the property, not an instant, and the moment it becomes a timestamp it acquires a timezone it does not have. Converting a stay date to UTC and back shifts the night by one in every zone east or west far enough, which is how a booking reports three nights while the property bills four. It gets worse at DST boundaries: a local time can be skipped entirely in spring, occur twice in autumn, and in zones that transition at 00:00 there is no local midnight at all on the transition date, so a naive start-of-day computation either throws or silently lands on the wrong instant. Store stay dates as DATE alongside the property's IANA zone, derive instants only where one is genuinely required (cutoffs, deadlines, audit), and keep civil date and instant as distinct types so the compiler catches the mixing.
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.
Abandoning working code to chase the optimal solution
Get the straightforward version correct, state its complexity, and only then optimise, keeping the working version until the faster one passes the same cases. A correct quadratic solution with a stated path to linear beats a half-written optimal one that never ran.
Listing technologies instead of trade-offs
Name the property the design needs first, such as ordered range scans, multi-entity transactions, cheap appends, or a predictable p99, then pick something that provides it and say what it gives up in exchange. Almost any component is defensible once you state the requirement it satisfies and the one it sacrifices.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Reconcile booking totals against night-grain amounts
Two unsorted inputs. booking(booking_id, total_minor, currency_code) has up to 2,000,000 rows. booking_night(booking_id, stay_date, night_price_minor, tax_minor, fee_minor, state) has up to 10,000,000 rows, state in held, confirmed, cancelled, consumed, no_show. For every booking the sum of night_price_minor + tax_minor + fee_minor over its non-cancelled nights must equal total_minor. Report every booking_id where it does not, with the signed difference in minor units, and separately report booking_ids present in only one input. Target O(N + B) time. State your space bound.
Approach
- Stream
booking_nightonce into a hash map keyed by booking_id, accumulatingnight_price_minor + tax_minor + fee_minorin a signed 64-bit integer and contributing zero for rows in statecancelled. Create the entry on every night row, cancelled ones included: that is what keeps a booking whose nights are all cancelled distinguishable from a booking with no night rows at all. Minor-unit totals sit far below the int64 ceiling of about 9.2 x 10^18, so no decimal or big-integer type is needed, and keying strictly by booking_id is also what keeps two currencies from ever landing in one accumulator. - Stream
bookingonce, look up each booking_id, emit the signed difference when it disagrees withtotal_minor, and remove the map entry as you consume it. - What is left in the map afterwards is nights-without-a-booking; bookings that found no entry are bookings-without-nights. Both are one-sided keys and both are real defects, so the report iterates both key sets. A booking whose nights were all cancelled has a legitimate sum of zero against a non-zero total, and it only appears if you walk the booking side.
- Time O(N + B), space O(B): roughly 2 x 10^6 int64 accumulators plus keys, tens of megabytes, which fits one process. If B outgrows memory, hash-partition both inputs on
hash(booking_id) mod Pand run the identical algorithm per partition — every key lands in exactly one partition, so correctness is unchanged and peak memory falls by P. - Trade-off worth stating: the single-pass version needs the nights side to fit in memory as an aggregate, while a sort-merge on booking_id needs only streaming memory but costs O(N log N) comparisons. At N = 10^7 the hash version wins; the sort version is the answer when the nights side is a hundred times larger.
Follow-up
- The report runs while bookings are being written. How do you choose a cut point so a booking mid-write is not reported as a variance?
- Per-night tax rounds per jurisdiction. If the sum of rounded night amounts legitimately differs from the booking total by one or two minor units, is that a variance or a modelling bug, and what do you change?
- How would you extend this to detect a booking whose night rows do not cover the contiguous range [check_in_date, check_out_date) exactly once?
Expand a supplier rate push into unit-date rows
Parse a supplier availability and rate push. Each line is ARI|<supplier_unit_code>|<start>/<end>|<dow_mask>|<seq>|<k=v>,... where the date range is inclusive at both ends, dow_mask is seven characters for Monday through Sunday, and keys come from price, cur, minlos, maxlos, cta, ctd, stop, avail. Expand each line into (unit_type_id, stay_date) updates carrying supplier_seq = seq, dropping any row whose stored seq is greater than or equal to seq. Prices arrive as decimal strings and must become minor units at the currency's ISO 4217 exponent. Reject malformed lines with a reason. Target O(input characters + rows emitted).
Approach
- Split each line on
|and validate the field count before interpreting any field. A line with the wrong arity is rejected whole; lenient parsing here produces rows that look plausible and are wrong, which is worse than a rejection nobody can miss. - Parse
startandendas civil dates and iterate with a civil-date successor, never by adding 86,400,000 ms to an instant. Validateend >= startand that the span is at most the 450-day horizon before expanding, so a malformed range cannot drive an unbounded loop. - Compute the weekday of each civil date directly — a civil date has exactly one weekday and needs no timezone to determine it — and index the mask with Monday at position 0. Apply the mask before doing any per-row work.
- Convert the price by shifting the decimal point, not by float multiplication. Split on
., require the fractional part to be no longer thanexponent(cur), right-pad it to exactly that length, and concatenate.12.5with KWD (exponent 3) becomes 12500 minor units;1250.00with JPY (exponent 0) is a rejection, because JPY has no fractional part to round into and silently truncating it changes the price by a factor of 100. - Apply the seq guard per
(unit_type_id, stay_date), not per message. A single date inside the range may already carry a newer delta while the rest of the line is fresh, and dropping or applying the whole line on one comparison is wrong in both directions. - Stream rows to the writer rather than buffering per line: one pass, O(characters) for parsing plus O(1) per emitted row, and O(1) working space independent of the 450-day maximum expansion.
Worked solution 30 min
- Take four lines. (1)
ARI|DLX-KING|2026-03-01/2026-03-07|1111100|88231|price=12500,cur=JPY,minlos=2,cta=1. (2)ARI|DLX-KING|2026-03-01/2026-03-07|0000011|88232|price=1250.00,cur=JPY. (3)ARI|STD-TWIN|2026-03-01/2026-03-03|1111111|41000|price=12.5,cur=KWD,avail=4. (4)ARI|GONE-999|2026-03-01/2026-03-02|1111111|900|price=100,cur=USD. - Establish the weekdays: 2026-03-01 is a Sunday, so the range runs Sun, Mon, Tue, Wed, Thu, Fri, Sat.
- Apply each mask and count the surviving dates per line before any other validation.
- Validate currencies and prices: JPY has exponent 0 and KWD has exponent 3, and DLX-KING and STD-TWIN are in the mapping table while GONE-999 is not.
- Apply the per-row seq guard against a stored map containing exactly one entry, (DLX-KING, 2026-03-04) -> 88300, and tally the final counts.
Follow-up
- The same message arrives twice, and once out of order relative to a later delta. Show which rows change on each delivery.
- A supplier sends
minlos=3on the middle night of a range. Which stay is affected, given that minimum length of stay is read from the arrival night's row? - How would you report a line that is syntactically valid but semantically absurd — a price two orders of magnitude off the unit's recent range — without blocking the rest of the message?
Find stay dates where committed units exceed capacity
For one unit_type_id with known physical_unit_count and oversell_allowance, you are given up to 200,000 confirmed booking rows as (check_in_date, check_out_date, units) where nights are the half-open range [check_in_date, check_out_date), plus active inventory_hold_night rows as (stay_date, units). The horizon is at most 500 consecutive stay dates. Return every stay date on which confirmed booking units plus active hold units exceed physical_unit_count + oversell_allowance, with the committed count and the excess on each. Target O(M + D) time and O(D) space.
Approach
- Build a difference array over the horizon. For each confirmed booking add
+unitsatcheck_in_dateand-unitsatcheck_out_date. Because nights are half-open, the checkout date is where consumption stops rather than a night that is consumed, so the negative delta lands exactly on it with no offset. - Normalise the holds into the same array: each active hold night is a one-night interval, so
+unitsatstay_dateand-unitsat the next civil date. The input arrives at two different grains, and converting both to deltas is what lets a single sweep handle the mixture without a join. - Index the array densely by
stay_date - horizon_startrather than using a sorted map. That makes the pass O(M + D) rather than O(M log M), and at D <= 500 the array is a few kilobytes, so the dense choice is free. - Prefix-sum left to right; the running total at index i is the committed units on that stay date. Compare each against
physical_unit_count + oversell_allowance— the allowance is part of the invariant, not a fudge factor applied afterwards — and emit the breaches with their excess. - Filter the inputs correctly at read time: cancelled bookings and non-active holds are excluded, and so are the hold rows of a booking that has already converted. During the conversion window the same units legitimately exist as both an
activehold and aconfirmedbooking night, and counting both manufactures a breach that is not real. - Deltas that fall outside the horizon still matter. A booking that starts before
horizon_startcontributes to every night inside it, so clamp its+unitsto index 0 instead of dropping the booking, or the first days of the horizon will read low.
Follow-up
- Make this incremental: a single new booking arrives. What do you recompute, and what does that cost compared with the full sweep?
- Run it across 500,000 unit types instead of one. Where does the dense-array choice stop paying, and what would you partition on?
- A breach is found. How do you decide whether it came from a real oversell, a double-counted conversion, or a hold the sweeper failed to expire?
Add a held-units column and constraint to a live calendar
ari_daily holds about 225 million rows, 500,000 unit types over a 450-day horizon, and is upserted continuously by the ingest path while the search path reads it. You must add held_units SMALLINT NOT NULL DEFAULT 0, a CHECK that remaining_units - held_units >= 0, a unique index the hold path needs, and a backfill of held_units from inventory_hold_night, with no window of unavailability. Give the ordered statement plan, the lock each step takes, and how you stop one long-running query from turning a millisecond DDL into an outage.
Approach
- Order the steps so each is cheap in isolation. Adding a column with a constant default has been a catalogue-only change since PostgreSQL 11 and does not rewrite 225 million rows, while a volatile default such as now() still does — so the constant is the whole trick. Add the column, deploy the code that maintains it, backfill, and only then validate the constraint.
- Add the CHECK as NOT VALID first. That takes a brief ACCESS EXCLUSIVE lock, skips the scan and begins enforcing new writes immediately; VALIDATE CONSTRAINT afterwards takes only SHARE UPDATE EXCLUSIVE and scans without blocking readers or writers. Adding it valid in one step instead scans every row while holding the table exclusively.
- Never add the unique constraint directly, because that builds the index under ACCESS EXCLUSIVE. Use CREATE UNIQUE INDEX CONCURRENTLY, which cannot run inside a transaction block, makes two passes over the table and can leave an invalid index behind if it fails — which must be dropped concurrently and retried rather than ignored. Then adopt it with ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX, which takes a brief exclusive lock and no rebuild.
- Defend every exclusive moment with a lock timeout and a retry loop. A pending ACCESS EXCLUSIVE request queues behind one long-running SELECT, and every later lock request queues behind that, so a reporting query or a session idle in transaction converts a millisecond ALTER into a full stall on the hottest table you own. Set lock_timeout to something like 100ms, retry with backoff, and inspect the activity view for the transaction you are about to queue behind.
- Backfill in bounded batches keyed on the primary key, committing each one. A single UPDATE across 225 million rows writes a new tuple version per row, roughly doubling table size before autovacuum catches up, holds one transaction open for hours, and blocks the ingest path on every row it has already touched. Batch by unit_type_id ranges, pause between batches, and watch dead tuple counts and replication lag rather than wall-clock progress.
- Account for what the column costs per row. New columns are appended and aligned, so a smallint can cost more than two bytes depending on what precedes it, and because every upsert already rewrites the whole tuple, that width is paid on every write to the table from then on rather than once at migration time.
Worked solution 40 min
- Clone a representative slice, add the column with a constant default, and confirm from timing and table size that no rewrite occurred.
- Add the CHECK as NOT VALID, insert a violating row to prove it is enforcing, then validate it and observe the lock level taken.
- Start a long SELECT in one session and run the ALTER in another with and without lock_timeout, watching the queue build behind it.
- Run the backfill in batches while the ingest path is writing, recording dead tuples and replication lag per batch.
Follow-up
- The concurrent index build fails at eighty percent. What is on disk, what does the planner do with it, and what is your next statement?
- A physical replica serves part of the search traffic. What does replaying this migration do to queries running there?
- How do you make the backfill resumable after it is killed at batch four thousand of five thousand, without rescanning what it already did?
Reconcile night revenue against captures without fanning either out
booking(booking_id, unit_type_id, state, total_minor, currency_code) has booking_night(booking_id, stay_date, unit_type_id, night_price_minor, tax_minor, fee_minor, state) one-to-many, and payment_transaction(payment_transaction_id, booking_id, kind, state, amount_minor) one-to-many. A report joins all three and sums, and on a three-night booking with a deposit plus a balance capture both totals are wrong. Name both multiplications precisely, then write the query giving nights sold, night gross and net captured per property per stay month, and state how a booking-grain capture is attributed to the months its nights fall in.
Approach
- Name the cardinality: booking to booking_night is one-to-N and booking to payment_transaction is one-to-M, so joining both produces N times M rows per booking. Every night is counted M times and every payment N times, and the two columns are inflated by different factors, which is why the report looks plausible rather than obviously broken. The defect is aggregating a measure across a row set two children have expanded, not the SUM itself.
- Reject the reflexive fixes. DISTINCT and SUM(DISTINCT ...) de-duplicate values rather than rows, so two genuine nights priced identically collapse into one and the total becomes wrong in the other direction while looking tidier.
- Aggregate each child in its own CTE before joining anything, and filter on kind and state rather than on sign: the capture total is SUM(amount_minor) FILTER (WHERE kind = 'capture' AND state = 'succeeded') minus the same expression for refunds, grouped by booking_id. Authorisations are not revenue and 'unknown' is not a failure, so neither may be counted. SUM with FILTER returns NULL rather than zero when nothing matches, so wrap both sides in COALESCE before subtracting or a booking with no refund reports nothing at all.
- Face the grain mismatch instead of joining through it. Nights are the consumption grain and a capture is at booking grain, so no join makes them directly comparable. Either report the two at their own grains and reconcile explicitly, or allocate each booking's net capture across its nights in proportion to night gross, in integer minor units, distributing the remainder largest-remainder first with a stable tiebreak on stay_date.
- Take the month from booking_night.stay_date and never from booking.created_at, because one booking's nights routinely fall in two months and two tax periods. date_trunc over a date argument returns a timestamp, so cast back to date if the grouping key is meant to be one.
- Filter on booking_night.state rather than booking.state, since a booking can be partially cancelled and the night rows are where that is recorded. Assert that the sum of night price, tax and fee equals booking.total_minor per booking as a test, because per-night, per-jurisdiction rounding is exactly where that identity breaks.
Follow-up
- A refund lands two months after the stay. Which month's net moves, and does finance want the same answer as the payout file?
- The allocation leaves three minor units unassigned on a zero-decimal currency booking. Where do they go, and is the rule stable across re-runs?
- From the result alone, how would you prove that no booking was expanded by a join?
Schedule deposit captures months out and survive a rate limit
payment_timing is pay_now, deposit or pay_at_property. A deposit captures a fraction now and the balance at a policy date in the property's timezone, up to a year ahead; pay_at_property captures nothing. Design the scheduler: where future captures live, how a worker claims due work without two workers taking the same row, what happens to an authorisation that expires long before its capture date, and how the backlog behaves when the processor rate-limits you for an hour during a peak arrival week.
Approach
- Keep scheduled work in a table, not in a broker. payment_schedule(booking_id, kind, due_at_utc, state, attempts) with an index on (state, due_at_utc) is queryable, auditable and correctable; brokers are built for near-term delivery, and a year-long delay becomes in-flight state you cannot inspect, amend when the booking is amended, or cancel when the booking is.
- Claim with SELECT ... FROM payment_schedule WHERE state = 'due' AND due_at_utc <= now() ORDER BY due_at_utc FOR UPDATE SKIP LOCKED LIMIT 200 in PostgreSQL, flip the claimed rows to 'in_flight' and commit before calling the processor, so a crash leaves a row the reconciler can resolve by its stored key rather than a lock you have lost. SKIP LOCKED is what lets N workers share one table instead of serialising behind the head of the queue.
- Compute due_at_utc from the local policy expression at booking time and store both it and the expression. 'Balance due 14 days before arrival at 18:00 local' is a civil expression; resolving it lazily at run time re-derives it against whatever the zone's rules are then, and a zone that has changed its offset silently moves the charge. Choose the branch for an ambiguous or skipped local time deliberately rather than inheriting the datetime library's default.
- Treat the authorisation as perishable. Its validity is measured in days and is scheme and merchant-category dependent, with longer windows for lodging categories under some scheme rules, while the capture can be a year out. So the design cannot hold an authorisation and capture it later: schedule a re-authorisation shortly before the capture, treat its decline as a booking event with a dunning path and traveller contact, and never present the original authorisation as the guarantee.
- Apply backpressure at the source: a token bucket set to the processor's published rate, a bounded worker pool, and exponential backoff with full jitter on 429s. The backlog is the buffer, so measure it as the age of the oldest due row rather than the row count, and alert on age. Give scheduled captures their own worker pool and connection pool, separate from interactive authorisations, so an hour of rate limiting cannot starve checkout.
- State the tolerance you are buying: a capture may run hours late and must never run early, because capturing before the policy date is a chargeback with the customer in the right. Make 'never early' an assertion in the test suite, not a convention.
Worked solution 30 min
- Create 100,000 schedule rows spread over a year, 5,000 of them due within the hour, and run 8 workers claiming with FOR UPDATE SKIP LOCKED.
- Assert no row is claimed twice, and measure claim throughput with and without SKIP LOCKED.
- Make the processor stub return 429 for an hour and then recover; chart oldest-due-row age and interactive authorisation latency through the window.
- Schedule a capture for a date past its authorisation's expiry and run it with and without the re-authorisation step.
- Book a balance due at 18:00 local on a day crossing a DST transition in a zone that has one, and compare the stored instant with a lazily re-derived one.
Follow-up
- The booking is amended to a new arrival date, and the property's zone changes its DST rules next year. Which stored instants are now wrong, and how do you find them?
- A worker was network-partitioned rather than dead, so its row was claimed twice. What actually prevents the double capture?
- The oldest due row is 26 hours old. Is that an incident? Give your threshold and its justification.
Design a resumable reservation feed for partner reconciliation
A partner's nightly job pulls every booking changed since its last run to reconcile against its own ledger, while bookings are amended and cancelled continuously during the walk. The current endpoint is GET /bookings?updated_since=&limit=&offset=, and the partner reports both missing and duplicated rows. Specify the replacement: the ordering key, the cursor, the bounds and whether they are inclusive, what happens to a row that changes mid-walk, the delivery guarantee you offer and the obligation it places on the partner, and the answer the partner gets when it resumes from a stale cursor.
Approach
- Retire OFFSET on both counts. The engine must produce and discard n rows to serve page n, so walking N rows costs on the order of N²/limit in total work; and the page is defined relative to a result set that is being mutated, so a row updated mid-walk moves and is either skipped or returned twice. The partner's symptom is a property of the pagination scheme, not of their job.
- Order by a key that is total and stable, and push the comparison into the query: WHERE (updated_at_utc, booking_id) > (:t, :id) ORDER BY updated_at_utc, booking_id LIMIT :n, backed by a btree index on exactly that pair so each page is one index seek plus a short range scan. booking_id breaks ties, and ties are routine because bulk updates share a timestamp.
- Then fix what the tuple does not fix. updated_at_utc is stamped when the statement runs, but the row only becomes visible at commit, so a transaction stamped before your high-water mark that commits after you have read past it is invisible and never reappears. Either trail the watermark by more than the longest write transaction, or order the feed by a sequence assigned at commit — an outbox or change table — which removes the hazard by construction rather than by a timing assumption.
- State the guarantee rather than implying one: at-least-once, ordered by change, with no promise of exactly one delivery per row. That is what makes resumption safe, and it puts a matching obligation on the partner — key the apply on (booking_id, version) and treat a version already seen as a no-op.
- Make the cursor opaque and versioned — base64 of the position plus a schema tag — so the ordering key can change later without breaking stored cursors. Define expiry: a cursor older than the change feed's retention cannot be honoured, so answer 410 Gone carrying the earliest resumable position, rather than silently restarting from the beginning and handing the partner a full re-sync they did not ask for.
- Cap limit server-side and return the page's own high-water mark instead of a total count. A count over a live feed is expensive to compute and already wrong by the time it is serialised.
Follow-up
- The partner wants four workers walking in parallel. What do you hand them, and what breaks if they shard on booking_id modulo four?
- A booking is erased after the partner has already ingested it. How does the feed express that?
- How large can one page's payload become, and what do you do about a booking with four hundred nights?
Page latency jumps while every connector dashboard is green
Whole-page search p50 moved from 240ms to 2.7s and p99 from 900ms to 4.5s overnight. Twenty supplier connectors are called in parallel behind a 5-second per-call timeout, and the request returns only once all twenty have answered. The connector dashboard alerts on error rate and availability; both are unchanged and near zero, and the circuit breaker trips on error rate alone. Per-connector latency is charted but not alerted, and that chart shows one supplier's p50 at 2.6s against 120ms the day before, still returning HTTP 200 inside the timeout. Deliverable: the tail arithmetic for wait-for-all, why the breaker never fired, and the design that bounds the page.
Approach
- Decompose the page budget by stage -- destination resolution, local ARI evaluation, connector fan-out wait, pricing, ranking, serialisation -- before touching any connector, so you are certain the growth is in the wait and not in ranking or serialisation.
- Do the tail arithmetic explicitly, and use it as a consistency check on the baseline. Waiting on all N calls makes the page inherit the maximum, so page latency is pointwise at least every call's latency and every page quantile bounds the corresponding connector quantile: a 900ms baseline page p99 already told you no connector's p99 exceeded 900ms. Under independence P(page <= t) = F(t)^20, so the page's p99 sits at each call's p99.95, and the chance that at least one of twenty calls exceeds its own p99 is 1 - 0.99^20, about 18 percent. The independence assumption deserves its own check when connectors share a proxy, a resolver or a thread pool.
- Find the degraded dependency by median, not by errors. Wait-for-all converts one connector's median into the page's median, which is exactly the 240ms to 2.7s move -- a median shift is a systematic change in one dependency, not a tail event -- so rank connectors by the change in their median contribution to the wait. A supplier that answers 200 inside the 5-second timeout on all but a fraction of a percent of calls moves the error-rate breaker's input by almost nothing, which is why it never tripped and why the error dashboard reads green.
- Replace the fixed timeout with a budget: set a total deadline for the request and derive each call's timeout from the budget remaining at the instant that call is issued, so a late stage cannot spend time the page no longer has.
- Return partial results with an explicit completeness flag rather than stalling or failing the page, and propagate that flag into ranking and caching, because a result set missing one supplier is a different artefact from a complete one.
- Trip the breaker on latency and outstanding-request count as well as errors, so a degraded supplier stops consuming budget on every subsequent request, and probe it with a bounded half-open rate before closing.
Follow-up
- What does the completeness flag change for the traveller, and what does it change about whether that page can be cached?
- With a latency-triggered breaker, how do you prevent oscillation when the supplier sits at the threshold, and what half-open probe rate do you choose?
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Practice prompt ↗Practice prompt ↗03Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Practice prompt ↗Practice prompt ↗04Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Practice prompt ↗Practice prompt ↗06One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Practice prompt ↗07Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
When the requirements were thin, the interesting part is how you fenced the problem off: the assumption you wrote down, who you got to confirm it, the narrow version you shipped first so the rest stayed cheap to change. Guessing and being right is luck. Guessing in writing, where someone could correct you, is method.
How do you handle environments where safety and compliance protocols a…
How do you handle environments where safety and compliance protocols are non-negotiable?
Approach
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
How do you feel about adhering to strict American Cruise Lines rules w…
How do you feel about adhering to strict American Cruise Lines rules while employed aboard our vessels?
Approach
- Close with what you would do differently, concretely.
- Name the disagreement and how you resolved it with evidence.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
What draws you to a career in the maritime sector specifically?
What draws you to a career in the maritime sector specifically?
Approach
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?
Can you describe your familiarity with the maritime industry and how y…
Can you describe your familiarity with the maritime industry and how your technical experience applies to this domain?
Approach
- Pick a story where you made the decision, not one where you watched it.
- Give the blast radius: what could have broken, and what you measured.
- Close with what you would do differently, concretely.
Follow-up
- What did you decide not to do, and why?
- How did you know your change caused the improvement?
- 01
How do you handle environments where safety and compliance protocols are non-negotiable?
- 02
How do you feel about adhering to strict American Cruise Lines rules while employed aboard our vessels?
- 03
What draws you to a career in the maritime sector specifically?
- 04
Can you describe your familiarity with the maritime industry and how your technical experience applies to this domain?
Is this an official American Cruise Lines interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at American Cruise Lines. Rounds and questions reflect what candidates have reported, not a process American Cruise Lines has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How difficult is the interview process?
Candidates generally report the process as straightforward and manageable. The focus is on finding a strong cultural and operational match rather than testing you with complex, abstract puzzles.
PracHub interview research ↗What differentiates successful candidates?
Successful candidates are those who show a clear understanding of the maritime industry and demonstrate a high level of comfort with the structured, rule-based nature of American Cruise Lines.
PracHub interview research ↗How should I prepare for the initial interview?
Focus on your professional history and be prepared to explain your interest in the maritime industry. Being detail-oriented and articulate will go a long way.
PracHub interview research ↗Is there a specific technical focus?
Candidates report that while core engineering skills are essential, the assessment prioritizes your ability to apply those skills to American Cruise Lines' maritime infrastructure.
PracHub interview research ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
platform · Accessed 2026-09-22 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
platform · Accessed 2026-09-22 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
platform · Accessed 2026-09-22