Interview guides · Retail Solutions
Retail Solutions Software Engineer Interview Guide
Retail Solutions software engineer prep on databases: indexes, query plans, and the anomalies an isolation level still permits.
The 60-second brief
As a Software Engineer at Retail Solutions, you play a pivotal role in shaping the technological landscape of the company. This position is essential for developing innovative solutions that enhance customer experiences and streamline operational processes. Your work will directly impact the effectiveness of various products and services, helping the company maintain its competitive edge in the retail industry.
The behavioural round is a technical round in narrative form. Prepare it by collecting specifics you actually owned, such as a design you argued against, an incident you diagnosed, or a decision you later reversed, rather than by rehearsing phrasing.
Focus areas
- Reconcile payment state after timeouts instead of retrying
- Make checkout idempotent from cart to capture
- Design order state machines with explicit compensating transactions
Overview
In this role, you will contribute to diverse projects, ranging from backend system enhancements to frontend application design. You will collaborate with cross-functional teams, including product managers and UX designers, to create solutions that are not only functional but also user-friendly. The complexity of the systems you will be working on, combined with the scale of the user base, makes this role both challenging and rewarding. You will have the opportunity to influence key business strategies and drive significant improvements in the company's technology stack.
The Retail Solutions interview process
Retail Solutions candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Initial Screening Call
The title covers product work, platform work, infrastructure, mobile and frontend, and those are different jobs with different loops behind them. A screening call is the cheapest place to find out which one the seat is, and asking reads as experienced rather than fussy. The questions that separate them: what the team is on call for, what the last three projects were, and whether any round happens inside an existing repository instead of a blank file. Then say which of that you have done and which you have not. Claiming the whole posting is the fastest way to be found out one round later.
What to demonstrate
- Whether you can locate your experience inside one flavour of the role honestly instead of claiming the entire requirements list
- Whether you name what you have not done, which an experienced screener reads as a level signal and can plan the loop around
- Whether what you want next matches what the seat is: someone who wants greenfield work landing on a team that mostly operates an existing system is a hire that leaves within the year
How to prepare
- Mark every line of the posting as done, adjacent or new, and write one sentence for each adjacent line naming the closest thing you actually built
- Split your last two years into rough percentages across feature work, operating and debugging live systems, and design or review, so a question about scope gets numbers rather than adjectives
- Bring three questions that discriminate between seats: what the team is paged for, how much of the work is changing existing code versus standing up something new, and what shipped in the last quarter
Technical Assessments
The same problem is scored by two different mechanisms depending on the format, and preparing for one does not cover the other. With a person watching, partial progress is visible and a hint is a correction you can absorb; silence is the expensive failure, because nobody can read a half-written function. With an automated grader there is no partial credit for what you were about to do, nobody to ask, and the worked examples in the prompt are the entire specification. Read them as a contract, down to whether an empty result should be an empty list or no output at all.
What to demonstrate
- In a live session, whether your commentary tracks what your hands are doing, and whether a hint redirects you or gets defended against
- In an automated one, whether you cover the cases the examples do not show, since the hidden cases are where the score moves
- Whether you manage the clock on purpose: abandoning an approach that is not converging while there is still time to write something simpler that finishes
How to prepare
- Have someone hand you a problem and feed you one deliberately wrong hint. Practise testing it against a concrete case instead of accepting or rejecting it on authority.
- Do one timed run a week in a plain browser editor with autocomplete, linting and your own snippets switched off, which is closer to what these environments give you
- For the automated format, write the harness before the solution: a main that feeds the worked examples plus an empty and a single-element case and prints expected against actual, so a wrong submission is caught by you first
Onsite Interviews
Coding rounds mostly set a floor. They decide whether you clear the bar, not where you land on the ladder. Level tends to come out of the design discussion and the ownership stories, so the question worth auditing beforehand is whether the scope you describe matches the scope of the job. Work that stops at your own service, or a story whose hard part was writing the code rather than getting several people to agree on an interface, reads a level below where you think you are interviewing, and that gap is usually resolved downwards.
What to demonstrate
- Whether the largest thing you describe owning ran end to end — the decision, the migration path, the rollout, and what you did when it went wrong — or stopped at the change you merged
- Whether design answers include what you would not build, what you would defer, and what you would measure before committing, rather than only what the boxes are
- Whether a disagreement in a story was settled with something checkable — a benchmark, a prototype, a written proposal — instead of by seniority or by waiting it out
- Whether you can say which calls you made alone and which you escalated, and why the line sat where it did
How to prepare
- Write your largest piece of owned work as a timeline of decisions — who decided what, when, and what you did when the plan broke — then delete every sentence whose subject is "we" and see how much survives
- Take one system you know well and drill the migration answer: how old and new paths run side by side under live traffic, how you compare their outputs, what the rollback is once writes are going to both, and which step you would not automate
- Map each line of the ladder in the job posting to a specific thing you have done, find the line you cannot support, and prepare the closest evidence you have plus an honest account of the gap
Where candidates lose points
Letting a flash sale converge on a single inventory row inside a long transaction
Every checkout for the same variant updates the same inventory_position row, and row-level locks serialize them: the ceiling is roughly one update per lock hold time, where hold time runs from lock acquisition to commit, not from the UPDATE statement. At a 2-5ms hold that is a few hundred updates per second on that key, and adding application instances only lengthens the queue of waiters while pinning connections. The amplifier is putting an external call inside the transaction — a 300ms payment authorization between the decrement and the COMMIT drops the ceiling to about three per second and takes the connection pool with it. The fixes each cost something concrete: sharding the counter into N sub-rows with random selection makes 'how many are left' a SUM over N rows and introduces false negatives when units strand in one shard; serialising per-variant through a single-writer partition removes contention but bounds latency by queue depth; an admission-control token bucket in front keeps the database healthy but must return a real waiting state to the customer rather than an error.
Applying order state changes as blind writes when events arrive at-least-once and out of order
A cancel request and a 'picked' event travel different paths with no shared ordering, and a message bus that guarantees at-least-once delivery will redeliver either one after a consumer crash. Read-modify-write handlers then let the later writer win arbitrarily, so you ship a cancelled order or refuse to cancel one still sitting on a shelf, and both outcomes are expensive. Every transition needs to be a compare-and-set on the expected current state with an explicit branch for zero affected rows, plus a processed-event table keyed on the producer's event id so that a redelivery is a no-op rather than a second application. Exactly-once delivery is not available over an unreliable network; at-least-once delivery combined with idempotent effects is, and conflating the two is what produces handlers that are correct only on the first run.
Writing code before the input contract is pinned down
Before the first line, state the types, the size bounds, whether duplicates, negatives or an empty input are possible, whether the input is sorted, whether you may mutate it, and what the function returns when nothing matches. Every one of those answers changes the code, and discovering one at minute twenty costs a rewrite you no longer have time for.
Not asking what the system looks like if it dies halfway through
For any multi-step write, say what state remains if the process stops between step two and step three, and what brings it back: a single transaction, a saga with compensating actions, an outbox, or a reconciliation job. Partial failure is routine at any real call volume, so 'that shouldn't happen' is an answer with nothing behind it.
Questions & practice
Describe how you would approach solving a problem using dynamic programming.
Medium · data structures and algorithms
Approach
- State the target complexity and say which constraint rules the naive version out.
- Choose the data structure from the access pattern, not from familiarity.
- Name the brute-force solution and its complexity before improving on it.
Follow-up
- Which test case would catch an off-by-one here?
- How does this change if the input no longer fits in memory?
Explain the difference between breadth-first search and depth-first search.
Medium · data structures and algorithms
Approach
- Name the brute-force solution and its complexity before improving on it.
- Choose the data structure from the access pattern, not from familiarity.
- Restate the input: its shape, its size, and what is guaranteed about it.
Follow-up
- How does this change if the input no longer fits in memory?
- What is the worst case, and how likely is it on real data?
Write a function to reverse a string in Java.
Medium · data structures and algorithms
Approach
- Walk one small example through your approach before writing the whole thing.
- Restate the input: its shape, its size, and what is guaranteed about it.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- Which test case would catch an off-by-one here?
- What is the worst case, and how likely is it on real data?
Given an array of integers, find two numbers that add up to a specific target.
Medium · data structures and algorithms
Approach
- Choose the data structure from the access pattern, not from familiarity.
- Walk one small example through your approach before writing the whole thing.
- Name the brute-force solution and its complexity before improving on it.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Roll up facet counts over a category graph without double counting
Medium · dag · topological sort · distinct counting · sketches
Categories form a directed acyclic graph of up to 100,000 nodes and 300,000 child-to-parent edges, and a category may have several parents. Two million active variants each sit in exactly one category. For every category, return the number of distinct active variants in it or any descendant, and refuse to produce counts at all if a supplier feed has introduced a cycle. The memory budget is one gigabyte. State your complexity and where exactness is lost, if it is.
Approach
- Run Kahn's algorithm first: repeatedly remove zero-in-degree nodes, and if any node remains the graph contains a cycle. It costs O(V+E) and hands you the topological order the rollup needs anyway. Report the residual node set so the feed owner sees which edges close the cycle, rather than an assertion that the feed is bad.
- Show why addition is wrong here. On a tree, counts accumulate exactly in reverse topological order. On a DAG, a category reachable from an ancestor by two paths contributes twice, so a straight sum overstates every node above a diamond — and the overstatement is largest at the high-traffic parent categories, which is where a wrong number is most visible.
- Exact distinct counting needs set union. A bitset per node is 2,000,000 bits, or 250 KB, and 100,000 nodes is 25 GB — twenty-five times the budget. Compute that number and abandon the approach explicitly instead of hand-waving past it.
- Use HyperLogLog and merge in reverse topological order. Union is lossless because it is the register-wise maximum, which is exactly the property that makes it safe on a DAG where one variant arrives by two paths. At m = 4,096 registers the relative standard error is 1.04/sqrt(m), about 1.6%, at roughly 4 KB per sketch — about 400 MB for 100,000 nodes, inside budget. At m = 1,024 it is 3.25% error and about 100 MB.
- Complexity: O(V + E) traversal with one sketch merge per edge, O(V*m) space. Then state the exactness policy rather than leaving it implicit: keep exact sets below a cardinality threshold and switch to sketches above it, because the pages where an off-by-a-few count is noticeable are the small ones.
Worked solution (35 min)
- Build a 12-node DAG containing one diamond, assign variants to leaves by hand, and write down the exact per-node distinct counts.
- Implement the additive rollup and confirm it overstates at and above the diamond by exactly the shared subtree's variant count.
- Implement the HLL rollup at m = 4,096 and compare against exact counts on the toy graph and on a generated 100,000-node graph.
- Add an edge that closes a cycle and confirm the job refuses with the residual node list rather than looping or emitting partial counts.
Expected result: Additive and sketch rollups agree on every node below the diamond. Above it, the additive result is high by exactly the shared subtree's variant count while the sketch stays within roughly 1.6% relative standard error. Cyclic input produces a refusal naming the nodes involved.
Verify it yourself
- On the toy graph, every node's sketch count is within one unit of a brute-force distinct union computed with real sets.
- No parent's count is below the count of any of its children.
- Removing the diamond's second edge makes the additive result equal the exact result everywhere, confirming the diamond was the only source of double counting.
Follow-up
- A facet count and the filtered result count differ by 1.4%. What do you show the customer, and which of the two numbers do you fix?
- One variant moves between categories. What must be recomputed, and can it be done incrementally?
- Counts must now exclude variants with zero ATP at every node. Where does that predicate live, and what does it do to your refresh cadence?
Retire a variant without breaking historical order lines
Easy · soft deletes · audit history · referential integrity
product_variant has status in draft, active, discontinued, blocked, and sku_code UNIQUE. order_line references variant_id and already stores title_snapshot and unit_price_minor. A team proposes adding deleted_at to product_variant, hard-deleting anything discontinued for a year, and reusing the freed sku_code values. Say what each of those three does to order_line and to the unique constraint, give the DDL you would actually ship including any audit table, and write the query listing unshipped lines on confirmed orders whose variant is no longer active.
Approach
- Kill the hard delete on referential grounds rather than on principle. order_line.variant_id is a foreign key, so a DELETE either fails under the default NO ACTION or, with ON DELETE CASCADE, silently removes paid order history. Neither of those is a retention policy.
- Point at title_snapshot and unit_price_minor: that denormalisation is exactly what makes retirement survivable, because rendering an old order never joins product_variant. The row then only has to persist for the foreign key and for analytics, not for display, which is a much weaker requirement than the team assumed.
- Prefer the status enum that already exists over a parallel deleted_at. Two independent ways for a row to be dead means every query needs both predicates, and the one somebody forgets is the one that resurfaces a blocked variant in a facet or a sitemap.
- If soft delete ships anyway, the unique constraint must become a partial unique index — UNIQUE (sku_code) WHERE deleted_at IS NULL — or a retired row holds the code forever. Then argue against reuse on its merits: sku_code is the warehouse- and supplier-facing identifier, so reusing it makes every historical pick, return and supplier invoice ambiguous about which product it refers to.
- Put history in product_variant_audit at one row per change, keyed (variant_id, version), carrying the changed columns and changed_at_utc, written by the single writer or a trigger. For as-of questions either keep valid_from_utc and valid_to_utc with an EXCLUDE constraint on (variant_id WITH =, validity WITH &&) using btree_gist to forbid overlaps, or reconstruct by taking the newest audit row at or before the timestamp — the first costs a write-side constraint, the second costs a per-query sort.
Worked solution (25 min)
- Attempt a DELETE of a variant referenced by order_line and record the exact error and the constraint name it reports.
- Add deleted_at, mark one variant deleted, and try to insert a new variant reusing its sku_code; record the unique violation.
- Replace UNIQUE (sku_code) with a partial unique index WHERE deleted_at IS NULL and repeat the insert.
- Write and run the unshipped-lines query joining order_line to customer_order and product_variant, filtering order_line.state in ('reserved','released_to_node') and product_variant.status <> 'active'.
- Mark a variant discontinued rather than deleted and confirm the query still returns its lines.
Expected result: The DELETE fails with a foreign-key violation naming order_line's constraint; it never cascades. Under plain UNIQUE, SKU reuse fails even though the old row is soft-deleted; under the partial unique index it succeeds, which is precisely the ambiguity you then argue against. The query returns one row per affected order_line with its order state and the variant's current status.
Verify it yourself
- Two soft-deleted rows can now share a sku_code under the partial index — insert a second and confirm it is permitted, then decide whether that is acceptable.
- EXPLAIN the order-history read path and confirm product_variant does not appear in the plan at all, which is the snapshot columns doing their job.
- The query keys on order_line.state and product_variant.status, not on deleted_at, so it keeps working whichever retirement mechanism ships.
Follow-up
- A returns agent needs the variant's hazmat flag as it stood on the shipment date. Which of the two history shapes answers that in one index seek, and what does the other cost?
- An erasure request arrives for a customer, not a variant. Which columns on customer_order and order_line can be erased while the financial record stays reconcilable?
- Merchandising wants discontinued variants gone from search within a minute. What event do you publish, and what does the consumer do with it?
Write the reserve statement that cannot oversell a variant
Medium · isolation levels · compare-and-set · lock contention
inventory_position, keyed (variant_id, node_id), holds on_hand_units, reserved_units, damaged_units, safety_stock_units, oversell_allowance_units and CHECK (reserved_units <= on_hand_units - damaged_units + oversell_allowance_units). inventory_reservation holds reservation_id, variant_id, node_id, owner_type, owner_id, units, state, idempotency_key UNIQUE. Write the exact statements one reserve call runs, name the isolation level, and say what a retry of the same call does. Then explain why SELECT the position, subtract in application code, UPDATE to the result loses units, and give the throughput ceiling for one contended row.
Approach
- Write available-to-promise as an expression, never a column: on_hand_units - damaged_units - reserved_units - safety_stock_units + oversell_allowance_units, with in_transit_units included only when the promise date is beyond expected_receipt_at_utc. Note that the CHECK constraint is deliberately looser than that policy because it ignores safety stock: the constraint is the hard oversell floor, the WHERE clause is the commercial one, and they are allowed to differ.
- Claim idempotency first. INSERT INTO inventory_reservation (...) VALUES (...) ON CONFLICT (idempotency_key) DO NOTHING RETURNING reservation_id. Zero rows returned means this call is a replay, so read the existing row, return it, and touch no counter. Doing this before the counter update is what makes a retried reserve a no-op instead of a second hold.
- Bind the units in one statement: UPDATE inventory_position SET reserved_units = reserved_units + $n, updated_at_utc = now() WHERE variant_id = $v AND node_id = $k AND on_hand_units - damaged_units - reserved_units - safety_stock_units + oversell_allowance_units >= $n. Judge it by rows affected; zero is a refusal, and rolling the transaction back drops the reservation row so the caller may genuinely retry later — a refusal spends nothing.
- Say why that is safe at READ COMMITTED in PostgreSQL: the UPDATE blocks on the row lock and, when the blocking transaction commits, re-evaluates its WHERE clause against the updated row version rather than against the snapshot it began with. Split into SELECT then UPDATE and the two statements take separate snapshots, both read the same available count, and both write — a lost update that READ COMMITTED permits by design. REPEATABLE READ does not silently fix it; it converts it into a serialization failure (SQLSTATE 40001) that somebody has to retry, and InnoDB's REPEATABLE READ does not abort at all, so identical code behaves differently on a different engine.
- Give the ceiling as arithmetic. The row lock is held from the UPDATE until COMMIT, so a 2 ms hold caps that one key near 500 commits per second and a 5 ms hold near 200, regardless of how many application instances exist — extra instances only lengthen the waiter queue and pin connections. Put a 300 ms payment authorization inside the same transaction and the ceiling falls to roughly three per second, which is the single most common way this design is destroyed.
- Price each mitigation rather than listing them: sharding one row into N sub-rows makes remaining stock a SUM over N and strands units in shards nobody selects; a single-writer partition per variant removes contention but bounds latency by queue depth; admission control protects the database only if the customer is shown a real waiting state instead of an error.
Follow-up
- The expiry sweeper releases a hold at the same instant the order service commits it. Write both statements so a committed reservation can never be expired.
- reserved_units has drifted 40 units above the sum of active reservations on one row. Write the reconciliation query, and say what it should do when it finds that at 3am.
- Two nodes can both satisfy the line. Where does node selection happen, and what does it do to your single-row contention argument?
How do you ensure data security in your applications?
Medium · system design and architecture
Approach
- Name the failure you are designing for, then the recovery path.
- State the consistency you need, and where you are willing to be stale.
- Fix the scope first: who calls this, how often, and what they do when it fails.
Follow-up
- What would you drop to keep the system up under load?
- What breaks first when traffic grows ten times?
How would you architect a real-time chat application?
Medium · system design and architecture
Approach
- Fix the scope first: who calls this, how often, and what they do when it fails.
- Name the read and write paths separately; they rarely have the same bottleneck.
- Name the failure you are designing for, then the recovery path.
Follow-up
- How does this behave when that dependency is down for an hour?
- What breaks first when traffic grows ten times?
A customer reports a bug in one of our applications. How would you approach troubleshooting it?
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- 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?
Evolve the order representation without breaking installed clients
Hard · versioning · backward compatibility · deprecation
GET /orders/{id} is consumed by a storefront you deploy, a mobile app with a long tail of versions you cannot force to upgrade, and partner integrations you do not control. Three changes are queued: order_line.state gains a new value, grand_total_minor is replaced by a money object carrying amount and currency, and the line array must become paginated for a four-hundred-line order. Define the versioning scheme, the compatibility rules inside a version, and the migration for each of the three. Include how you establish that the last old client is gone.
Approach
- Classify the three changes, because they are not the same kind of change. A new enum value breaks any client that switches exhaustively, and whether it breaks is decided by a tolerance rule you either published on day one or cannot retrofit. Replacing a scalar with an object is unambiguously breaking. Paginating an array breaks anyone who reads the array's length, which is everyone who ever summed it.
- Choose the versioning axis and accept its cost. A version in the path is trivially routable and cacheable but forks handlers and invites duplication; a dated version in a request header pins each client to a snapshot and lets one handler serve many, at the cost of a transformation chain that needs testing per version pair. The rule that matters more than the choice: inside a version, additive only. New optional fields yes; removals, type changes, and newly tightened validation no.
- Ship the money change additively. Emit both grand_total_minor and total: {amount_minor, currency_code} for the whole deprecation window, mark the old field deprecated in the published schema, and derive one from the other at exactly one point so the two cannot disagree during the overlap.
- Ship pagination as a new representation rather than a mutation of the old one: keep lines inline with a documented cap in the existing version, add a lines sub-resource with its own cursor, and add a lines_truncated boolean so an old client can at least detect the case it cannot handle. Silently returning a truncated array is worse than any error, because the client computes a wrong total and reports it to a customer.
- Ship the enum value against the tolerance rule if one exists. If it does not, the honest options are a version bump, or mapping the new state onto the nearest old value for old clients while stating which lie you are telling: mapping on_hold_fraud onto pending_payment keeps old clients out of the shipped path, which is the safe direction to be wrong in.
- Instrument the retirement, and be clear about what you cannot see: the server observes requests, never which response fields a client reads, so a per-client-version request counter keyed on a required client identifier is a proxy and not proof. Pair it with Deprecation and Sunset (RFC 8594) headers carrying the retirement date and a rehearsal in which a sampled slice of old-version traffic receives 410 for an hour, then complete the removal.
Worked solution (35 min)
- Build the compatibility matrix: for each of the three changes, name which client breaks and at which line of its own code.
- Choose the versioning axis and write the single paragraph of contract text that states the additive-only rule inside a version.
- Write the dual-field emission for money and name the one place the derivation happens.
- Write the pagination migration including the truncation flag and the new sub-resource, and note when the flag has to ship relative to the first oversized order.
- Write the retirement plan: the client identifier, the counter, the response headers, the sampled dark period, and the date.
Expected result: A scheme in which both unambiguously breaking changes are delivered additively inside the current version, the enum change is delivered under an explicit tolerance rule or a version bump where none exists, and removal is gated on per-version traffic counters plus a rehearsed short outage rather than a calendar date alone.
Verify it yourself
- A client compiled against today's schema and never rebuilt parses every response the new server emits.
- Your plan names the proxy it uses for 'nobody reads this field any more' and states what that proxy can miss, since response-field reads are not observable server-side.
- The truncation flag exists in the old representation before any order is large enough to trigger it.
- No step in the plan tightens validation on a field that already exists.
Follow-up
- A partner pins to a dated version from two years ago and never moves. What is the enforcement mechanism, as opposed to the policy?
- How do you keep transformation chains for five old versions tested without maintaining five copies of every test?
- One client fetches an order and PUTs the representation back. What happens to the fields its version does not know about, and does PATCH actually fix it?
Order history page slows as its line count grows
Easy · n+1 · batching · latency
The account order-history endpoint returns a customer's last 20 customer_order rows with their order_line rows, and for each line renders a display title read from product_variant plus tracking read from shipment. p95 is 380 ms; the single-order view is 40 ms. The database logs 157 statements for one request, none slower than 1 ms, with flat CPU and no bad plans. Diagnose the latency, name the second defect the same code path carries, give the fix, and state the statement count you expect afterwards.
Approach
- Read the shape out of the count before forming a theory. Twenty orders averaging 3.4 lines gives 68 lines, and 1 + 20 + 68 + 68 = 157: one driver query, one per order, two per line. Every statement being sub-millisecond rules out plans, locks and data volume, so the time is round trips.
- Check the arithmetic accounts for the whole regression before fixing anything. 156 extra round trips at roughly 2 ms each is about 310 ms on top of a 40 ms baseline, which covers the observed 380 ms. Had it covered only half, there would be a second latency defect and the batching fix alone would disappoint.
- Identify the per-row statements by their normalised text: a lines-by-order_id lookup, a single-row product_variant lookup keyed by variant_id, and a single-row shipment lookup keyed by shipment_id. Confirm they are lazy relationship loads by dropping those two fields from the response and watching the count fall to 21.
- Rewrite the round trips away first, one batched statement per level. Select the orders, then SELECT ... FROM order_line WHERE order_id = ANY($1), then SELECT ... FROM product_variant WHERE variant_id = ANY($1), then SELECT ... FROM shipment WHERE shipment_id = ANY($1), and join them into maps in application memory. That is four statements, down from 157, and the count is now independent of both orders and lines per order.
- Name the correctness defect the batching accidentally exposes: the display title is being joined back to product_variant, which is mutable and carries no validity interval, so a renamed or discontinued variant silently rewrites a historical order. order_line.title_snapshot and unit_price_minor exist precisely so the order does not read the live catalogue. Read the snapshot and the product_variant batch disappears entirely, leaving three statements: the orders, their lines, and the shipments.
Follow-up
- The endpoint filters on customer_id. What happens to guest orders, which carry customer_id NULL and are identified by contact_email?
- A wholesale account has 4,000 orders. What breaks about ANY($1) and keyset pagination here, and in which order do you fix them?
- No statement was slow, so no slow-query alert fired. What signal would have caught this before a customer did?
Suggested practice checklist
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Day 1 — Measure before reasoning
- Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
- Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
- Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.
Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.
Day 2 — References, copies, and the bugs they produce
- Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
- Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
- Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.
Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.
Day 3 — Types, once, in a language that checks them
- Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
- Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
- Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.
Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.
Day 4 — Concurrency, starting with what actually runs at the same time
- Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
- Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
- Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.
Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.
Day 5 — Debugging as a procedure rather than an instinct
- Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
- Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
- Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.
Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.
Day 6 — Tests that catch the bug you are about to write
- Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
- Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
- Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.
Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.
Day 7 — Debug something broken, out loud
- Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
- Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
- Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.
Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.
What the behavioural round is actually measuring
Every story you tell gets read for blast radius and judgement: what could have broken, who else it touched, what you knew at the moment you decided. Nobody can audit your code in an hour, so they audit your reasoning instead. Pick work where the call was genuinely yours and the consequences were real enough to remember.
Can you provide an example of how you have handled a conflict within a team?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Pick a story where you made the decision, not one where you watched it.
- Close with what you would do differently, concretely.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
What motivates you to achieve your goals as a software engineer?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
- 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 would you do differently if you ran that again?
Argue against holding reservations in a cache with a TTL
Hard · cache coherence · reservations · design review · disagreement
A lead specifies that checkout should hold reservations in an in-memory cache with a fifteen-minute TTL and write back to inventory_position asynchronously, on the grounds that the database row is the bottleneck. You believe it is wrong and you have been assigned to build it. Describe a time you argued against a design you were told to implement. State the failure you predicted, the evidence you brought, how long the disagreement ran, what you did once the decision went against you, and what production eventually showed. Include the version of your argument that persuaded nobody.
Approach
- Establish the failure precisely, and note that there are two independent ones. First, replication in a typical in-memory store is asynchronous, so a failover can promote a replica that is missing writes already acknowledged to the client, and here those lost writes are holds on units customers are mid-payment for. Second, and separately, a TTL that expires while an authorization is in flight releases units to another buyer, producing an oversell that no component logs as an error.
- Pre-empt the durability counter-argument, because that is what keeps the design alive: an append-only file flushed once a second bounds loss on a single node to roughly a second of writes and says nothing about what a failover discards. Durability knobs on a single node do not make a replicated cache a transactional store.
- Attack the premise rather than the taste, because the premise was a performance claim and is therefore measurable. The claimed bottleneck is one row's lock hold time, which runs from lock acquisition to commit: at a 2-5ms hold that is a few hundred updates per second on that key, and the number collapses only if something slow sits inside the transaction. Measure the current hold time and find out whether the external call is inside it before redesigning around the symptom.
- Bring evidence the decision-maker can check in a day, not a quarter: the measured p99 hold time on the hot row, and a count of committed reservation units exceeding the position for any variant over the last week. An argument that costs the other person one query is the one that moves.
- State the alternative in one sentence with its cost owned: the cache is a negative filter that may say 'definitely none left' and never 'yes, it is yours', the binding decision is a conditional UPDATE in the same store as the reservation rows judged by affected-row count, and the cost is that you now confront the real single-key ceiling and must pay for sharding, single-writer partitioning or admission control if it binds.
- Describe the disagree-and-commit mechanics concretely: what you built, what you instrumented so the prediction was falsifiable, and what threshold would have proved you wrong. Report the outcome including the chance you overstated severity, and keep 'I was right' separate from 'the disagreement was handled well'.
Follow-up
- You lost the argument. What do you instrument so the question is settled by data in a month rather than by another meeting?
- Suppose the measurement shows the row genuinely tops out below the drop's arrival rate. Which mitigation do you pick, and what does it break?
- What evidence would have made you drop the objection entirely?