As a Software Engineer at Jet2 and Jet2holidays, you are at the heart of an industry-leading travel organization that relies heavily on robust, scalable technology to deliver seamless holiday experiences to millions of customers. Your work directly impacts the digital infrastructure that powers everything from flight bookings and hotel inventory management to dynamic customer communications.
This role is both challenging and rewarding, requiring a balance of technical rigor and a deep understanding of the travel domain. You will work within engineering teams to build, maintain, and optimize software solutions, often operating in an Agile environment. Because Jet2 and Jet2holidays operates at significant scale, your ability to write efficient code and contribute to system architecture is critical to supporting the company’s ongoing growth and operational reliability.
Initial Screening
reportedThe 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
Behavioral Assessment
reportedYour first answer is not really what is scored. It buys the follow-up questions, and those decide the round. An interviewer with fifteen minutes takes one thread and pushes on it four or five times, so a story you can only tell at a single level of detail collapses under the third why. That is an argument for fewer stories known deeply rather than one prepared per prompt. Four or five pieces of work you can still explain down to the code you changed and the argument you had about it will cover nearly anything asked in this round.
What to demonstrate
- Whether a story holds as the questioning moves from what you did to why that instead of the alternative, and then to what you would change knowing what you know now
- Whether you can re-cut a project to answer the question actually asked rather than delivering a rehearsed block that answers an adjacent one
- Whether your level of detail is chosen rather than habitual: going down to the schema when the question is about the data model, staying out of it when the question is about the person who disagreed with you
How to prepare
- Pick four projects and write the chain out four levels deep for each: what you did, why that, why not the alternative, and what would have to be true for the alternative to have won. Where you cannot reach the fourth level, you have a placeholder rather than a story
- Have someone ask why three times in a row on a single thread with nothing else added, and mark the point where you start repeating a sentence you already said. That point is where the interviewer stops learning anything
- Build a one-page index instead of an answer bank: the common prompts in this round (disagreement, a failure that was yours, thin requirements, a deadline you missed, work you inherited) mapped to which of your four projects you would use for each, so the choosing is done now rather than while an interviewer waits
Technical Discussions
reportedMost of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Structured Assessment Centers
reportedInput bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.
What to demonstrate
- Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
- Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
- Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
- Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply
How to prepare
- For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
- For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
- Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last
Final Assessment
reportedCoding 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
PracHub editorial advice for the preparation topics above.
Modelling availability as a boolean
Bookability is a count plus a set of per-night restrictions whose scopes differ, and collapsing that into is_available breaks in both directions at once. Closed-to-arrival applies only to the arrival night; closed-to-departure applies only to the checkout date, which is not itself a booked night; stop-sell applies to every night in the range; minimum length of stay is conventionally read from the arrival night's row, though implementations genuinely differ and that is the first thing to pin down with a supplier. A boolean drops stays that are legal (a range whose middle night is closed to arrival is perfectly bookable) and sells stays that are not, and the second kind does not fail in search, it fails at supplier confirm after the traveller has been charged.
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.
Reading the constraints as preamble rather than as part of the problem
The bounds are usually there to eliminate the obvious approach: n up to 10^5 makes an O(n^2) scan roughly 10^10 operations, far outside any per-test time budget, and an input larger than memory rules out loading it at all. When a bound is not given, ask for it, then say out loud which approach it kills.
Trusting input because it came from your own front end
Anything crossing a trust boundary is hostile: parameterise queries instead of building SQL by concatenation, validate against an allow-list rather than a deny-list, and bound the size of anything you allocate from a request. Raising this unprompted in an API or design question is a cheap and unusually strong signal.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Peak distinct unit types requested in a sliding minute
An append-only search log for one client gives events (ts_ms, unit_type_id) with non-decreasing ts_ms, up to 5,000,000 events. Automated scraping is characterised by breadth rather than volume: a person reloads one property, a crawler walks a catalogue. Return the maximum number of distinct unit_type_id values this client requested inside any 60,000 ms window under the half-open convention [t, t + 60000), and the window start at which that maximum is first reached. Target O(N) time, with space proportional to the distinct values alive in one window rather than to N.
Approach
- Two indices over the array. Advance
right, incrementcount[unit_type_id], and increment a separatedistinctcounter only on the 0 -> 1 transition. - Shrink from the left while
ts[right] - ts[left] >= 60000: decrement the count, decrementdistincton the 1 -> 0 transition, and erase the key. The half-open convention makes the condition>=rather than>; with>the window spans 60,000 ms inclusive and every reported peak drifts by the events sitting exactly on the boundary. - Track the running maximum of
distinctand thets[left]at which it was first reached, updating only on a strict increase so ties resolve to the earliest window. - Each index is admitted and evicted exactly once, so the total work is O(N) amortized even though the eviction is a while loop inside the scan. Erasing at count zero is what keeps space at window-distinct rather than session-distinct; leaving zero-count keys behind turns the map into an O(N) structure over a long session.
- Trade-off: this requires the client's events in timestamp order in one place. If they arrive out of order across ingest partitions, two pointers are invalid — you need a watermark plus a reorder buffer sized to the expected skew, which raises the memory bound from window-distinct to skew-distinct and adds the watermark delay to detection latency.
Worked solution 20 min
- Take seven events: (0, A), (10000, B), (20000, A), (30000, C), (59999, D), (60000, E), (61000, A).
- Walk
rightfrom 0 to 6, maintainingcount,distinctand the left pointer, and write the window contents at each step. - Note the one eviction: at right = 5,
60000 - 0 >= 60000, so left advances from 0 to 1 and A's count drops from 2 to 1 without changingdistinct. - Record the maximum distinct value and the
ts[left]at the step where it is first reached. - Separately compute the maximum event count over the same windows and compare the two answers.
Follow-up
- Make it streaming with bounded memory and approximate distinctness. What does HyperLogLog cost you in accuracy at the thresholds you would actually alert on, and can you still evict from the left?
- The same client_id is shared by a corporate NAT. What second signal would you combine with breadth before acting, and why is breadth alone not enough?
- How does the answer change if you need the peak over all clients simultaneously rather than one at a time?
Order saga compensations over the steps that actually ran
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
- 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.
- 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.
- For compensation, build the induced subgraph over the steps in
completedorunknown, 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 bareremaining_units = remaining_units + nrelease applied against a hold that was never taken hands out capacity that does not exist. - An
unknownstep 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. - 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.
- 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?
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.
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?
Stop two checkouts claiming the last unit on one night
ari_daily(unit_type_id, stay_date, remaining_units) has PRIMARY KEY (unit_type_id, stay_date) and CHECK (remaining_units >= 0); unit_type carries physical_unit_count and oversell_allowance; inventory_hold_night(hold_id, stay_date, unit_type_id, units, state, expires_at_utc) records claims, and availability is currently computed as remaining_units minus active holds. Two checkouts each ask for two units over 2026-03-12 to 2026-03-15 while three remain. Write the statements that take the hold atomically, name the isolation level you are assuming and what PostgreSQL does to your statement under it, and give the release path.
Approach
- Notice first that the invariant as stated cannot be enforced by a single-row compare-and-set: remaining_units sits on ari_daily while the held quantity is an aggregate over another table, and no one statement spans both. Materialise held_units on the ari_daily row so the check collapses onto one row, and keep inventory_hold_night as the per-hold ledger that makes release idempotent. That is a deliberate denormalisation with a cost, not a shortcut.
- Claim each night with one conditional UPDATE whose predicate is the capacity check: UPDATE ari_daily SET held_units = held_units + n WHERE unit_type_id = u AND stay_date = d AND remaining_units - held_units >= n. Inspect the row count of every statement before committing; zero means the night is gone and the transaction rolls back rather than continuing with a partial hold. A SELECT followed by an UPDATE cannot work, because both readers see three units before either writes.
- Issue one statement per night in ascending stay_date order, and across unit types by unit_type_id then stay_date. A single multi-row UPDATE locks rows in whatever order the chosen plan returns them, and an index scan and a bitmap heap scan answer that differently, so plan shape silently becomes your lock order. Two overlapping ranges taken in request order deadlock, and one side dies with SQLSTATE 40P01.
- Name the isolation level as a decision rather than inherit it. Under READ COMMITTED a blocked UPDATE re-reads the newly committed row version and re-evaluates its WHERE clause, so the loser's predicate now fails and it updates zero rows with no retry needed. Under REPEATABLE READ or SERIALIZABLE the same statement instead raises serialization_failure, SQLSTATE 40001, and the caller must retry the whole transaction within a bounded budget. Same SQL, two different code paths.
- Make release a state transition that gates the decrement: UPDATE inventory_hold_night SET state = 'released' WHERE hold_id = h AND stay_date = d AND state = 'active', and subtract from held_units only when that statement reports one row, in the same transaction. A bare subtraction is not idempotent, and an expiry sweeper racing a conversion applies it twice, selling a unit that does not exist.
- Treat the CHECK as the last line of defence rather than the control. If it ever fires you get a constraint violation on a booking that should have been refused cleanly, and the ceiling is physical_unit_count plus oversell_allowance, so the predicate has to compare against the allowance-inclusive figure rather than against raw capacity.
Follow-up
- Holds expire on wall-clock time that no transaction observes. Does the read ignore expired holds or does a sweeper release them, and what does each choice cost in oversell versus understated availability?
- Under REPEATABLE READ, what is your retry budget, and how do you stop a retry storm from making the contention worse?
- The third night fails after the first two succeeded. What have you already written, and what does the compensation look like?
Decide whether stay-range availability is derived or precomputed
Searches outnumber bookings by two to three orders of magnitude. One result page with plus or minus three days of flexibility expands to roughly 10^3 (unit type, range) evaluations over 10^3 to 10^4 rows of ari_daily(unit_type_id, stay_date, remaining_units, restrictions), against a whole-page p99 near one second. Decide whether bookability for a length of stay is derived from the unit-date rows at request time or materialised. Size both options with real numbers over a 450-day horizon and a 30-night maximum stay, and specify the invalidation path for a single supplier push.
Approach
- Size the materialised option before arguing about it. A row per (unit type, start date, length of stay) over a 450-day horizon with stays up to 30 nights is about 13,500 rows per unit type against 450, a thirty-fold increase in a table the ingest path already rewrites millions of times a day. The killer is not the build but the invalidation: a change to one night invalidates every range covering it, which is the sum of one through thirty, or 465 entries per unit type per changed night.
- Size the derived option honestly too. A thousand candidates times up to seven date offsets times four nights is on the order of 10^4 narrow rows per page, served by short index scans on the primary key — a few megabytes of buffer traffic and tens of milliseconds warm. That fits the budget only while those rows are resident, so the real question is the working set and its eviction behaviour, not the row count.
- Choose the grain that composes. Cache at (unit type, stay date), bounded by unit types times horizon, and compose ranges in memory at request time: every entry is reused by every length of stay and every flexibility offset that touches that night, and one supplier push invalidates exactly the unit-dates it wrote, one entry each, with no range expansion at all.
- Reject the request-shaped cache explicitly. Keying on destination, dates, length of stay, party composition, currency, point of sale and promotion eligibility is a cartesian product whose hit rate is near zero outside the head of the distribution, and any input omitted from the key serves one traveller another traveller's price — a class of defect customers report before monitoring notices it.
- Permit one denormalisation for pruning only, and make it conservative. A coarse per-(unit type, month) has-any-availability summary may over-report but must never under-report, used to skip candidates before the exact evaluation runs. A false positive costs one wasted range scan; a false negative hides bookable inventory and is invisible in every metric you currently have.
- State the cost of stale in the currency that matters. Availability served a few minutes old converts into supplier rejections at confirm time, after the card has been authorised, so the freshness target follows from the rejection rate you will tolerate. The measurement that settles the design is rejection rate plotted against the age of the availability behind each offer, not cache hit rate.
Worked solution 40 min
- Compute rows per unit type for both designs over 450 days and stays of one to thirty nights, and the invalidation count for a single night's change in each.
- Measure the derived path end to end with a warm cache and a cold one, recording buffer counts from the analysed plan.
- Replay one day of real search shapes against a per-night cache and against a request-key cache and compare hit rates on identical traffic.
- Plot confirm-time rejection rate against the age of the availability that produced each offer.
Follow-up
- Denormalising listing_status onto ari_daily would make the availability scan index-only. What does delisting one unit type cost then, and whose writes does it contend with?
- A popular date falls out of cache during a flash sale. How do you stop two thousand concurrent searches all recomputing it at once?
- How would you measure whether the per-night cache is genuinely reused across lengths of stay rather than filled once per request and discarded?
How do you approach testing and the defect cycle in your code?
How do you approach testing and the defect cycle in your code?
Approach
- Clarify what is being asked and what a complete answer contains.
- Work from the requirement backwards to the design.
- Say what you would check first and why it is the highest-information step.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
What is the technology stack used in your previous project?
What is the technology stack used in your previous project?
Approach
- Work from the requirement backwards to the design.
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Hold three nights of the last unit without overselling
Design the hold service. Input: unit_type_id, check_in, check_out, units. It must hold this invariant: for every (unit_type, stay_date), confirmed booking nights plus active holds never exceed physical_unit_count + oversell_allowance. Tables are ari_daily(unit_type_id, stay_date, remaining_units) and inventory_hold_night(hold_id, stay_date, unit_type_id, units, state, expires_at_utc). Two travellers request the same last unit over overlapping three-night ranges in the same millisecond. Give the exact statements, the lock order, the isolation level you assume and what that engine does to your statement under it, and how hold expiry is evaluated.
Approach
- One conditional UPDATE per night, never a read followed by a write: UPDATE ari_daily SET remaining_units = remaining_units - :units WHERE unit_type_id = :u AND stay_date = :d AND remaining_units >= :units. Inspect the row count of every statement before commit; a zero means that night lost and the whole transaction rolls back. The CHECK (remaining_units >= 0) catches a bug in this logic, it is not the concurrency control.
- Issue the nights in ascending stay_date order inside one transaction and insert all inventory_hold_night rows in the same transaction, so a partially written hold is impossible. Locking in request order instead is where two overlapping multi-night holds take the same row locks in opposite directions and deadlock; PostgreSQL detects that after deadlock_timeout (1s by default) and aborts one side with SQLSTATE 40P01, which shows up as a latency cliff rather than as wrong data, and one second is most of a checkout budget.
- Name the isolation level and its consequence in that engine. Under PostgreSQL READ COMMITTED, a blocked UPDATE re-reads the newly committed row version and re-evaluates its WHERE clause, so remaining_units >= :units is tested against the committed value and the guard holds. Under REPEATABLE READ or SERIALIZABLE the same statement instead raises a serialisation failure (SQLSTATE 40001) that the caller must catch and retry with a bounded budget. Choose READ COMMITTED here and say why: the guard lives in the statement, not in a snapshot read earlier in the transaction.
- Decide expiry deliberately, because with decrement-on-hold the units are only genuinely back when an increment runs. Sweeper-only is simple and understates availability for up to one sweep interval, which is a lost sale on exactly the dates that sell out. Clock-evaluated reclamation adds the units of active holds whose expires_at_utc <= now() back into the availability read via the partial index on (unit_type_id, stay_date) WHERE state = 'active', and the booking path runs the gated release for those holds in the same transaction as its own decrement, so the number it read is the number it can take.
- Make release an idempotent state transition gating an increment: UPDATE inventory_hold_night SET state = 'released' WHERE hold_id = :h AND stay_date = :d AND state = 'active', and increment ari_daily only when that statement reports exactly one row, in the same transaction. A bare increment is applied twice by a retried release, or by a sweeper racing a conversion, and sells a unit that does not physically exist.
- Set the TTL from data rather than roundness: too short and travellers lose the unit while typing a card number, too long and a sold-out date strands inventory in dead holds. Measure the checkout duration distribution and set the TTL near its p95, then track stranded-unit-minutes per sold-out date as the cost you accepted.
Worked solution 30 min
- Seed one unit type with remaining_units = 1 on three consecutive dates.
- Fire 200 concurrent three-night hold requests for that range; count successes and failures and read remaining_units on each date afterwards.
- Re-run with half the clients locking nights in reverse request order and record aborts, their SQLSTATE and their latency.
- Re-run under REPEATABLE READ without a retry loop, then with a bounded retry, recording the error class and success count for each.
- Let the winning hold expire without running the sweeper, read availability, then run the reclamation path and read again.
Follow-up
- Write out the interleaving for the deadlock: two three-night holds over overlapping ranges, locked in request order.
- Nights 1 and 2 decrement successfully and night 3 returns zero rows. What does the client see, and what is in the database one second later?
- oversell_allowance is 2. Where does it appear in your statement, who sets it, and on what evidence?
Multi-night holds deadlock during a flash sale
During a promotion the booking path's failure rate rises from 0.1% to 6%. PostgreSQL logs SQLSTATE 40P01 with two statements named per incident, both of the form UPDATE ari_daily SET remaining_units = remaining_units - 1 WHERE unit_type_id = $1 AND stay_date = $2 AND remaining_units >= 1. Single-night stays are unaffected; only stays of two or more nights fail, in pairs, on one popular unit type. The nights are iterated from a hash map built out of the request payload. Deliverable: the interleaving, the fix, and why a retry-only mitigation is insufficient.
Approach
- Read the database's own deadlock report before the application log. The DETAIL names both backends, both statements and the tuples each waits on; confirming that the two statements touch the same (unit_type_id, stay_date) rows in the opposite order is the entire diagnosis and costs one log line.
- Trace the order back to its source. A hash map has no stable iteration order, so two overlapping ranges -- 14-16 March and 15-17 March -- can lock the 15th then the 16th in one request and the 16th then the 15th in the other. Each holds one row and waits on the other until the detector fires at deadlock_timeout and aborts one transaction.
- Fix the order rather than the symptom: sort nights ascending by stay_date and issue one conditional UPDATE per night, inspecting each row count before issuing the next. Do not collapse it into a single UPDATE ... WHERE stay_date = ANY($1): the executor takes row locks in plan order, which you do not control and which can change with statistics, and you also lose the per-night row count the capacity check depends on.
- Keep a bounded retry but change what it means. With a deterministic order, two correctly written hold paths cannot deadlock, so any remaining 40P01 indicates a third writer -- the expiry sweeper, an amendment, a supplier refresh -- not obeying the same order. Alert on it instead of absorbing it.
- State the isolation dependency explicitly, because the retry policy differs by level: under READ COMMITTED a blocked UPDATE re-reads the newly committed row and re-evaluates remaining_units >= 1, so the guard still holds; under REPEATABLE READ or SERIALIZABLE the same statement instead raises SQLSTATE 40001, which the caller must catch and retry with an explicit budget.
Follow-up
- The sweeper that expires holds also writes ari_daily. What order does it use, and what happens to your invariant if it batches its work by hold_id?
- Under SERIALIZABLE, how many retries do you allow, with what backoff, and what do you show the traveller when the budget is exhausted?
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Practice prompt ↗Practice prompt ↗03Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Practice prompt ↗Practice prompt ↗04Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Practice prompt ↗Practice prompt ↗06One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Practice prompt ↗Practice prompt ↗07Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.
Practice prompt ↗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 manage your work within an Agile framework?
How do you manage your work within an Agile framework?
Approach
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Can you describe a time you had to handle a difficult situation in a p…
Can you describe a time you had to handle a difficult situation in a professional environment?
Approach
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
How do you handle operational or management issues when they intersect…
How do you handle operational or management issues when they intersect with technical tasks?
Approach
- Close with what you would do differently, concretely.
- 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.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
Can you explain your experience with C# and SQL in a professional envi…
Can you explain your experience with C# and SQL in a professional environment?
Approach
- Pick a story where you made the decision, not one where you watched it.
- State the situation in two sentences and spend the rest on the reasoning.
- 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 manage your work within an Agile framework?
- 02
Can you describe a time you had to handle a difficult situation in a professional environment?
- 03
How do you handle operational or management issues when they intersect with technical tasks?
- 04
Can you explain your experience with C# and SQL in a professional environment?
Is this an official Jet2 and Jet2holidays interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Jet2 and Jet2holidays. Rounds and questions reflect what candidates have reported, not a process Jet2 and Jet2holidays has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗How long should I spend preparing for the interview?
Dedicate at least a week to reviewing your past projects and brushing up on core C# and SQL concepts. Familiarize yourself with the company’s services to show genuine interest.
PracHub interview research ↗What is the most important thing to emphasize?
Focus on your problem-solving process. Interviewers are interested in how you think, how you handle constraints, and how you learn from past technical challenges.
PracHub interview research ↗Is the technical interview difficult?
The difficulty is considered average for the industry. It is designed to test your actual, day-to-day engineering capabilities rather than abstract puzzles.
PracHub interview research ↗What should I know about the culture?
Jet2 and Jet2holidays is a fast-paced environment where reliability and team collaboration are highly valued. Show that you are a reliable team player who takes ownership of their work.
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