Interview guides · Commerce Pundit
Commerce Pundit Software Engineer Interview Questions & Guide 2026
Caching practice for Commerce Pundit software engineer prep: settle the staleness budget before choosing an invalidation strategy.
The 60-second brief
A Software Engineer at Commerce Pundit is a pivotal contributor tasked with building and maintaining high-performance e-commerce solutions. This role is central to the company’s mission of delivering scalable, robust digital experiences for clients, often involving complex integrations within the Magento ecosystem and modern front-end frameworks like React.
The title spans product, platform and infrastructure work, and which of those the seat actually is decides whether design or algorithms carries more weight in your preparation. The posting rarely settles it; what the team is on call for usually does.
Focus areas
- Derive available-to-promise across nodes and open reservations
- Design order state machines with explicit compensating transactions
- Keep flash-sale writes off one contended row
Overview
You will be expected to balance technical precision with a deep understanding of business requirements. Whether you are architecting back-end services or crafting responsive user interfaces, your work directly influences the reliability and success of client platforms. This position is ideal for engineers who thrive in fast-paced environments, value rigorous technical standards, and enjoy solving the intricate problems inherent in global e-commerce development.
The Commerce Pundit interview process
Commerce Pundit candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
HR Screening
Half of this call is the part candidates treat as small talk: start date, notice period, work authorisation and its timing, location and time zone, on-call, and the number. Those are what kill offers late, after several engineers have each spent a day. Surfacing a hard constraint now costs you nothing and occasionally buys you something, since a loop compressed to fit a competing deadline can usually only be arranged if it is asked for early. The common failure is deflecting the compensation question twice, then discovering at offer stage that the band never reached your number.
What to demonstrate
- Whether your hard constraints are compatible with the role before a loop gets booked: earliest start, notice period, what authorisation you hold and when it needs action, days on site, willingness to carry a pager
- Whether you give a compensation range with something behind it, such as current total compensation or a competing timeline, rather than leaving the band untested
- Whether your stated timeline is real, since a competing deadline raised now is something scheduling can sometimes work around and the same deadline raised at offer stage usually is not
How to prepare
- Write each constraint down in one line before the call and state them as facts rather than negotiating them live under a question you were not expecting
- Set your range from two or three current data points for that level and location, and name the structure you are quoting in, so the number is comparable to the one they are holding
- If another process is running, say where it stands and by when, and ask directly whether this loop can be scheduled inside that window
Technical Interviews
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
Management Discussion
Because the format is not fixed, the first job in the room is classification. Listen to the opening question and decide what it is: a probe into work you have already described, a fresh problem to solve now, or a conversation about how you operate. Each wants a different register, and the common failure is forcing a rehearsed structure onto a question that did not ask for it. Running a full design ritual on a ten-minute debugging question reads as not listening. When you cannot tell which it is, ask how long they want to spend and answer at that depth.
What to demonstrate
- Whether the shape of your answer matches the question, so a yes-or-no gets answered before it is justified and an open prompt gets a direction before a detour
- Whether you check how much depth is wanted instead of deciding for them, and whether you stop when the answer is complete rather than continuing until someone interrupts
- Whether you can be redirected in the middle of an answer without restarting it from the beginning
- Whether a question outside your experience gets an honest boundary followed by reasoning from what you do know, instead of a confident answer with nothing behind it
How to prepare
- Rehearse one project at three lengths, roughly thirty seconds, three minutes, and a full walkthrough at the depth of a design review, and practise switching between them when someone interrupts mid-telling
- Have someone ask you five questions of deliberately mixed type in one sitting without telling you the types, and score only whether you identified each one correctly before you started answering
- Draft the sentence you will use to check depth, along the lines of asking whether the short version is useful here or they want the detail, and use it in a real conversation this week so the day of the round is not its first outing
Where candidates lose points
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.
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.
Optimising an axis nobody named
Ask which resource is actually scarce here: wall-clock latency, throughput, memory footprint, cost per request, or engineering time. Shaving a constant factor off an in-memory step is wasted effort when the same function makes a blocking remote call inside the loop.
Choosing a schema before the access patterns are known
Write the queries first, with their filters, sort orders, cardinalities and which ones sit on the latency-critical path, then design tables and indexes to serve them. An index nothing queries still costs write throughput and storage, and a hot query with no supporting index becomes a full scan that only hurts once the table is big.
Questions & practice
Choose the best legal promotion combination for one cart
Hard · np-hard · graph decomposition · promotions · determinism
A cart has up to 200 lines and a subtotal S. Up to 2,000 promotion rules are active, each with an eligibility predicate over the cart, a discount function — a fixed amount off the subtotal or a percentage off it — and a set of rules it may not combine with. Percentage discounts compose, so a combination's total is not the sum of its members' individual discounts. Return the legal combination giving the largest total discount, deterministically, inside a 20 millisecond budget. Exhaustive enumeration is correct — explain precisely why it is unusable, and state every structural assumption the method you substitute depends on.
Approach
- Name the problem before optimising it. With arbitrary pairwise exclusions, choosing the best legal subset is maximum-weight independent set on the conflict graph, which is NP-hard in general — and under composing discounts there is not even a well-defined per-rule weight to hang on a vertex. 2^2000 is not a large constant to be absorbed by faster hardware; it is not a number any machine bounds.
- Collapse the input first. Evaluate eligibility once per rule against the cart at O(R*L) with early exit, and keep only the applicable rules. On a real cart that is typically 10 to 20, and every later cost is exponential in that number rather than in 2,000, which makes the filter the highest-leverage step in the whole design.
- Fix the composition order, then write the objective down as a formula rather than as a procedure. Fixed amounts before percentages, ties broken by ascending rule id, identical in the cart service and the order service — two services choosing different orders produce two totals for one cart and the quote-versus-capture comparison then fails at capture. A chosen set enters the result only through F, the sum of its fixed amounts clamped at S, and P, the product of its (1 - rate) factors: the total discount is D = S - P*(S - F). That reduction holds only while every applicable rule discounts the subtotal; rules scoped to overlapping line sets do not collapse to a scalar pair and must be scored by applying them.
- Connected components of the conflict graph are independent subproblems only when the objective is separable, and this one is not. Take S = 100, component one = {a 50-off fixed rule, a 60% rule} mutually exclusive, component two = {a 40-off fixed rule, a 50% rule} mutually exclusive, no edge between them. Each component's best choice in isolation is its percentage rule (60 beats 50, and 50 beats 40), but applying both leaves 1000.40.5 = 20 and a discount of 80, while taking both fixed rules leaves 100 - 90 = 10 and a discount of 90. A fixed amount is worth F*P, so its value depends on what the other component chose. Optimising components independently and adding the winners is wrong here by 10 on a 100 cart, and it is wrong in the direction that looks plausible.
- Use the components for what does factorise: the space of legal subsets, not the objective. The legal subsets of the applicable set are exactly the Cartesian product of each component's legal subsets, so enumerate each component's legal subsets at a cost of the sum of 2^c over components, reduce each to its (F_c, P_c) pair, and keep only the Pareto frontier — (F, P) dominates (F', P') when F >= F' and P <= P', which is sound because D is non-decreasing in F and non-increasing in P once F is clamped at S. Merge the frontiers pairwise, pruning after each merge. The result is exact and deterministic, and on a typical five-component profile the surviving frontier is tens of pairs against the 2^20, about 1.05 million, subsets of a flat search.
- Cap both the component size and the merged frontier width. Beyond roughly 20 members 2^c alone exceeds the budget, and a frontier truncated to a fixed width is no longer provably optimal, so in either case fall back to a deterministic greedy pass in precedence order and mark the result as not proven optimal rather than returning it silently. A discount one cent below optimal is a business decision; a nondeterministic discount is a defect.
- Close with the budget arithmetic instead of asserting speed. At around 20 applicable rules split into five components, the sum of 2^c is about 80 (F, P) evaluations, each a multiply and an add rather than a cart re-price; the frontier merge is a few hundred pair comparisons; and exactly one full cart application runs at the end, on the winner, which is also what you validate the exclusions against. That sits well inside 20 ms.
Follow-up
- A limited-quantity promotion has 50 uses left and 500 carts want it. Where does that counter live, and what happens when two carts read it at the same instant?
- The best combination recomputed at capture differs from the one on the quote. Which is honoured, and what does the customer see?
- A rule set grows to 20 applicable rules inside one conflict component. What do you ship?
Recompute reserved units and report drift per fulfilment node
Easy · aggregation · hashing · reconciliation · streaming
You are given two unsorted streams. inventory_position rows carry (variant_id, node_id, on_hand_units, reserved_units, damaged_units, safety_stock_units, oversell_allowance_units) — up to 40 million rows. inventory_reservation rows carry (reservation_id, variant_id, node_id, units, state, expires_at_utc) — up to 20 million rows. Recompute reserved units from the reservation rows, report every (variant_id, node_id) whose stored reserved_units disagrees, and emit available-to-promise for the rest. Define which reservation states you count. One pass over each stream, no re-reads. State your time and space bounds.
Approach
- Fix the predicate before writing any code: reserved is SUM(units) over rows with state IN ('held','committed'). The case that decides the design is a 'held' row already past
expires_at_utcthat the sweeper has not reached. Count it, because the reserve path's conditional UPDATE still counts it — a reconciliation job using a different predicate from the writer reports drift forever instead of converging. - Build one hash map keyed
(variant_id, node_id)to an int32 sum over the reservation stream: O(R) time, one entry per key holding live units. Bound it out loud — 20 million reservations is the upper bound on live keys, and at roughly 48-64 bytes per map entry that is order 1 GB. If the box is smaller, partition byhash(variant_id) % Pand run P passes for 1/P the memory at P times the read cost. - Stream
inventory_positionand probe, rather than sorting either side. Per row computeatp = on_hand - damaged - recomputed_reserved - safety_stock + oversell_allowance. Clamp to zero for display only and keep the raw signed value for the report: a negative raw ATP means the oversell invariant is already breached, which is a different alert from a drifted counter. - Emit drift as
(key, stored, recomputed, delta)rather than writing a correction inline. A repair from a scan is only safe if the position row has not moved since it was read, so pin both reads to one snapshot — in PostgreSQL a REPEATABLE READ transaction takes its snapshot at the first statement and every later statement sees it — and make the repairUPDATE ... WHERE reserved_units = <observed>, judged by affected rows, so a concurrent reserve makes it a no-op instead of reintroducing the drift. - Complexity: O(R + P) time, O(keys with live holds) space, two sequential passes, no sort. The drift check rides on the same pass as the ATP computation, which is the argument for doing both here rather than in two jobs that can disagree.
Worked solution (25 min)
- Write the state predicate and the ATP expression on one line each, and say explicitly which side an expired-but-unswept hold falls on.
- Build a 100,000-reservation fixture with three planted defects: one position row whose
reserved_unitsis 2 high, one that is 1 low, and one key present only in the reservation stream. - Run the job and confirm exactly three findings, with the expected signed deltas and the orphan reported separately from the drift.
- Re-run with a concurrent writer inserting a held reservation on the 2-high key, and confirm the guarded repair affects zero rows instead of clobbering the new hold.
- Measure peak resident memory, divide by the live-key count to get bytes per entry, and reconcile that against your estimate.
Expected result: Three findings and no others: a +2 drift, a -1 drift, and one orphan key reported in its own category. Every non-drifting key's recomputed reserved figure equals its stored value exactly, and ATP is computed from the recomputed figure rather than the stored one.
Verify it yourself
- Summing
unitsover live reservations for any single key by hand matches the job's recomputed value for that key. - No key appears both in the drift report and in the clean ATP output.
- Setting a position row's
oversell_allowance_unitsto zero turns at least one previously legal row negative in raw ATP, proving the allowance term is actually in the expression.
Follow-up
- A key appears in the reservation stream with no matching
inventory_positionrow. Is that drift, a broken foreign key, or a race with node onboarding — and what does the job do with it? - The job finds a delta of -3 at 03:00 on a variant that is mid-drop. Do you correct it, and what evidence makes that decision?
- Two copies of this job run concurrently after a deploy. What makes the second one harmless?
Find peak concurrently held units per variant with a sweep
Medium · sweep line · intervals · reservations
You have up to 50 million historical reservation records as (variant_id, node_id, units, start_utc, end_utc), where end_utc is the instant the hold stopped counting — its expiry, release or commit — and the interval is half-open. For each (variant_id, node_id) return the maximum units held simultaneously and the earliest instant that maximum was reached, then flag every key whose peak exceeded a supplied cap. Records are unsorted. State your complexity and your tie rule at equal timestamps.
Approach
- Expand each record into two endpoint events,
(start_utc, +units)and(end_utc, -units), group by(variant_id, node_id), and sort each group by(timestamp, delta_sign)with negative deltas first. That ordering is the entire correctness argument for half-open intervals: a hold ending exactly when another begins must not appear to overlap, and putting starts first invents a peak that never existed. - Sweep each group with a running sum, tracking the maximum and the timestamp at which it was first attained. The peak holds over
[t, next_event_time), so report the window rather than the instant — an alert naming a moment is far harder to corroborate against the ledger than one naming an interval. - Complexity: O(n log n) dominated by the sort, O(n) space, or O(n log g) when sorting within groups of size g. Grouping is not a micro-optimisation: a single global sort interleaves 50 million events from two million keys, and the running sum is meaningless unless it resets at every group boundary anyway.
- Offer the bucket alternative with its condition attached. If timestamps are second-granularity over a bounded window, a difference array over seconds makes each group O(n_g + W) for a window of W seconds. That wins for a hot variant with millions of holds and loses badly across a long tail of variants with three holds each, so choose per group by comparing n_g log n_g against n_g + W.
- Compare each peak against the cap
on_hand - damaged + oversell_allowanceas it stood at that time, not as it stands now. Judging last Tuesday's peak against today's on-hand produces false alarms on restocked variants and misses real breaches on depleted ones.
Follow-up
end_utcis NULL for holds still live at the end of the window. What do you substitute, and what does that choice do to the peak?- The same reservation appears twice because the export is at-least-once. Where do you dedupe, and on what key?
- You now need the peak per hour rather than per window. What changes, and what stays the same?
Explain why the reservation expiry sweeper stopped using its index
Medium · indexing · sargability · queue tables
inventory_reservation has 400 million rows and an index on (state, expires_at_utc). The sweeper runs SELECT reservation_id FROM inventory_reservation WHERE state = 'held' AND expires_at_utc < now() ORDER BY expires_at_utc LIMIT 500 FOR UPDATE SKIP LOCKED, then updates those rows to 'expired'. The backlog of expired held rows never drops below a few thousand, so the LIMIT is always satisfiable. It ran in 4 ms for months; during a promotion it went to 9 seconds and EXPLAIN now shows a sequential scan. Give four distinct explanations consistent with those facts, say how you would distinguish them, and give the index you would ship.
Approach
- Kill the selectivity answer first, because it is the one everybody reaches for and it does not survive the LIMIT. state = 'held' is an equality on the leading column and expires_at_utc < now() is a range on the second, so the scan opens at the start of the held group and walks forward already ordered by expires_at_utc; the range bound is a stop condition, not a filter. It touches roughly 500 index entries and 500 heap tuples whether held is 0.2 percent of the table or 40 percent of it. The competing plan has to sort 400 million rows before it can emit the first one, because Sort is blocking and a LIMIT cannot discount a startup cost. No distribution of held makes that cheaper. So a sequential scan here means the ordered path became unavailable, not that it was outbid, and every explanation worth having is about availability.
- First explanation: the planner cannot see a usable index. A CREATE INDEX CONCURRENTLY or REINDEX CONCURRENTLY that failed or was cancelled under promotion write load leaves pg_index.indisvalid = false, and the planner ignores such an index entirely while every write still maintains it, so write cost looks unchanged while reads fall off a cliff. Same class: a migration dropped or renamed it, or the table was partitioned and the index was only created on some partitions. Distinguish with indisvalid and indisready in pg_index, and with pg_stat_user_indexes.idx_scan, which stops incrementing at a timestamp you can line up against the deploy log.
- Second explanation: the statement stopped matching the index. Three shapes, all of which remove the ordered path rather than outrank it. The sort key or the range got wrapped — date_trunc('minute', expires_at_utc) < now(), or expires_at_utc::date — and no plain B-tree serves an expression. The leading column got wrapped or made collation-sensitive — state::text = $1, lower(state) = 'held', state LIKE 'held' under a non-C collation — so the equality that anchors the scan is gone and the index can neither bound nor order. Or the sweep was widened to state IN ('held','releasing'), which replaces that single equality with an array qualifier; whether nbtree can still return such a scan in index order is version-dependent (its array handling was reworked in PostgreSQL 17), so read EXPLAIN on your own version instead of asserting either way. Distinguish by diffing the normalised text in pg_stat_statements against what you believe is deployed, then EXPLAIN each candidate form side by side.
- Third explanation: the planner was not allowed to pick an index scan. enable_indexscan = off is the setting that reliably produces this shape; note that a merely hostile random_page_cost is not, for the same reason the selectivity story failed — the LIMIT means only about 500 random fetches are ever costed, so multiplying their price does not reach the cost of a full sort. Such a setting arrives from pg_db_role_setting, a connection pooler's startup options, or an ORM's per-session setup, which is why you read pg_settings name, setting, source and sourcefile from the sweeper's own connection rather than from your psql session. Those two sessions differing is the finding.
- Fourth explanation: the 9 seconds and the sequential scan are two incidents, and only one of them is a plan change. The sweeper updates state, which is indexed, so no update on this table can be a HOT update and every sweep leaves a dead index entry behind. Let a long-running transaction or a replication slot pin the xmin horizon and autovacuum cannot reclaim them, so the ordered scan walks a growing prefix of invisible entries to find 500 live ones, and FOR UPDATE SKIP LOCKED makes it read past every row the other sweeper instances hold on top of that. That is seconds of work with the index still in the plan, which means it explains the timing but not the EXPLAIN output, and saying so out loud is the point. Distinguish with n_dead_tup against n_live_tup, pg_stat_activity ordered by xact_start, pg_replication_slots.xmin, and EXPLAIN (ANALYZE, BUFFERS) buffer counts weighed against an output of 500 rows.
- Ship CREATE INDEX CONCURRENTLY ON inventory_reservation (expires_at_utc) WHERE state = 'held'. Moving the state predicate out of the key and into the index predicate leaves a key that is ordered by expires_at_utc globally, so the ORDER BY no longer depends on an equality surviving in the WHERE clause — and if the sweep is widened later, you widen the index predicate to WHERE state IN ('held','releasing') and keep the ordering, rather than losing it to an array qualifier in the key. It holds only rows the sweeper wants, so the walk never steps over committed or expired entries, and it shrinks as rows leave held, which bounds the dead prefix from the fourth explanation without removing it. Two preconditions: the planner must prove the query's predicate implies the index's at plan time, so 'held' has to reach it as a literal and not as a bound parameter inside a generic plan; and expires_at_utc must stay unwrapped in both the predicate and the ORDER BY. Say plainly that this index does not fix bloat — only vacuum and releasing the xmin horizon do.
Follow-up
- SKIP LOCKED is doing real work here. What happens to the ORDER BY and the LIMIT when 400 of the top 500 rows are already locked by a second sweeper?
- A committed reservation must never be expired. Where does that guard live so a second sweeper instance cannot break it?
- The sweeper's UPDATE is now the slowest statement in the system. Would you batch it differently, and what does each batch size do to replication lag?
Model variant attributes without a wide table or EAV
Easy · attribute modelling · gin indexes · grain
product_variant holds variant_id, product_id, sku_code, status, attributes JSONB, list_price_minor, currency_code, and nullable weight_grams, length_mm, width_mm, height_mm. One table must carry apparel (size, colour), an appliance (voltage, capacity_litres) and a consumable sold by weight. Give the DDL: which attributes you promote to real columns, which stay in attributes, and every index you create. Then write the facet query for active charcoal size-M variants priced between two bounds, and say which index serves which predicate and which predicate no index helps.
Approach
- Fix the grain before the storage. Description, category and reviews hang off product; anything reserved, picked, weighed or rated by a carrier hangs off the variant. Promote to real columns exactly the attributes with cross-category meaning or a non-containment access pattern: list_price_minor because it is range-filtered and sorted, and the four dimension columns because the sellable check tests them for NULL and carrier rating reads them. Category-specific facets stay in attributes.
- Reject the two extremes with their costs stated rather than by taste. A wide table needs a migration and an index per category; PostgreSQL caps a table near 1600 columns and mostly-NULL rows are cheap because of the null bitmap, so the real price is schema coupling, not bytes. EAV turns a k-facet filter into k self-joins or a GROUP BY with k FILTER clauses and HAVING, drops type checking, and leaves the planner estimating cardinality it cannot see.
- Index the document for the operator you actually issue. GIN with jsonb_path_ops serves containment (@>) and is smaller than the default jsonb_ops, but it does not serve key existence (?), ordering, or any range. That limitation is what decides which attributes had to become columns.
- Add a B-tree expression index for every attribute filtered by range, for example ((attributes->>'capacity_litres')::int). Two preconditions: the query must repeat the identical expression or the index is ignored, and the cast must be immutable — text to int is, text to timestamptz is not and PostgreSQL will refuse the index.
- Make the indexes partial on status = 'active', since only active is sellable, and expect the plan to be a BitmapAnd: PostgreSQL bitmap-scans the GIN for containment and the B-tree for the price range and intersects them. That is why two indexes beat the single composite index that cannot exist across a JSONB operator class and a scalar column.
Worked solution (30 min)
- Create the table with attributes JSONB, load 200,000 rows spread across the three categories, and populate capacity_litres on the appliance subset only.
- Create three indexes: GIN (attributes jsonb_path_ops) WHERE status = 'active', B-tree (list_price_minor) WHERE status = 'active', and a B-tree expression index on ((attributes->>'capacity_litres')::int).
- Run EXPLAIN (ANALYZE, BUFFERS) on the containment-plus-price-range facet query and record which indexes appear in the plan.
- Rewrite the range as attributes->>'capacity_litres' BETWEEN '100' AND '130' — text comparison, no cast — and re-run to watch the expression index drop out.
- Drop the jsonb_path_ops index, recreate it with default jsonb_ops, and compare pg_relation_size on both.
Expected result: The facet query plans as a BitmapAnd over the GIN containment scan and the B-tree price scan, with a heap recheck above it. The uncast text comparison plans as a sequential scan or a bitmap heap scan with a filter, and it also returns wrong rows: '1000' sorts before '130', so a 1000-litre variant falls inside a text band meant for 100 to 130. jsonb_path_ops is materially smaller than jsonb_ops over the same data.
Verify it yourself
- pg_relation_size on both GIN variants confirms jsonb_path_ops is smaller; a query using the ? existence operator falls back to a sequential scan against it rather than erroring.
- The plan shows Recheck Cond above the GIN scan, so rows are re-tested on the heap when the bitmap goes lossy; index and heap row counts differ.
- Removing status = 'active' from the query makes the partial indexes unusable — confirm the plan changes, which is the proof the predicate has to be provable at plan time.
Follow-up
- Facet counts have to come back with the result set. How do you compute the count per colour without scanning the filtered set twice, and what does an out-of-stock filter do to those counts?
- A supplier feed rewrites 500,000 attribute values in one run. What does that do to the GIN index and its pending list, and what would you change about the load?
- Someone wants ORDER BY capacity_litres DESC LIMIT 24. Which of your indexes serves that, and what happens on the rows where the key is absent?
Explain the design patterns used in Magento and their specific functions.
Medium
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?
Describe your process for setting up a server for a Magento 2 environment.
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.
- Work from the requirement backwards to the design.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Enforce a hard redemption cap on a limited promotion
Hard · consistency vs availability · lock contention · leases · partitioning
A promotion allows 10,000 redemptions, at most one per customer, evaluated by the cart service in two regions that share a primary database in one of them. During the drop the cross-region link degrades: the remote region's writes to the primary gain 400ms of latency and fail intermittently. Design the counter. Say where it lives, how a redemption is claimed and released when a cart is abandoned, what the remote region does while the link is degraded, and name the bound on overspend your design accepts. Explain which of consistency or availability you are giving up.
Approach
- Separate two constraints that look like one. 'At most one per customer' is a uniqueness problem, solved by a unique index on (promotion_id, customer_id) on the claim row. 'At most 10,000 in total' is a contention problem on a shared integer. The first does nothing for the second, and conflating them is the most common way a promotion overspends.
- Get the single-region shape right first: in one transaction, INSERT the claim ON CONFLICT DO NOTHING, then UPDATE promotion SET used = used + 1 WHERE promotion_id = $1 AND used < redemption_limit, judged by affected rows. At READ COMMITTED that single UPDATE is safe, because the statement re-evaluates its predicate against the updated row version after waiting on the lock; split into a SELECT and a later UPDATE it is a lost update. Release on cart abandonment by compare-and-set on the claim's state, so a double release cannot decrement twice.
- State the CAP choice out loud. A hard global cap with a single writer is CP: while the link is degraded the remote region either pays 400ms per claim or refuses redemptions, and for a cap that is a financial and often contractual commitment, refusing is the correct behaviour. Choosing availability here means choosing to overspend, and that is a decision for whoever owns the promotion budget, not for the service.
- If overspend is permitted, bound it with leases rather than with an eventually-consistent counter, because an eventual counter has no bound at all. The primary grants each region a lease of k units; a region sells only from its lease and renews as it drains. Overspend is zero while leases are honoured, and at most the unreturned lease if a region is partitioned and the primary reissues, so the worst case is (regions - 1) times k. That number belongs in the promotion's terms and in the runbook, not in a postmortem.
- Design the tail deliberately. As used approaches the limit, shrink k to 1 so the scheme degenerates into the contended single row on purpose: throughput in the middle, exactness at the boundary. Then specify what the customer sees when a claim fails at the boundary: the promotion is withdrawn, the cart re-prices, and the price quote is re-minted rather than the old discount being silently honoured.
Worked solution (40 min)
- Write the claim transaction as one INSERT plus one conditional UPDATE and identify the affected-row check that decides the outcome.
- State explicitly which property you are sacrificing during the degraded link, and what the remote region returns to customers under that choice.
- Design the lease protocol: grant size, renewal trigger, return-on-drain, and the primary's reissue policy after a partition.
- Compute the worst-case overspend as (regions - 1) times k and write it down in units and in currency.
- Specify the tail rule that shrinks k near the limit, and the customer-facing behaviour when a claim is refused.
Expected result: A claim path that is safe at READ COMMITTED because the decrement is a single predicated UPDATE; an explicit CP choice with refusal as the degraded behaviour, or a lease scheme whose maximum overspend is stated as a number before the drop rather than discovered after it; and a boundary rule that trades throughput for exactness as the counter approaches the limit.
Verify it yourself
- Concurrent claims never drive used above redemption_limit, verified by the affected-row semantics rather than by a subsequent report.
- The answer names which of consistency and availability is sacrificed, rather than claiming both.
- Maximum overspend is expressed as a specific quantity derived from the lease size and region count.
- A double release of the same claim leaves used unchanged the second time.
Follow-up
- Pick k, then compute the worst-case overspend and the number of lease renewals during the drop.
- A region is partitioned with 380 of 500 leased units unsold. What does the primary do at minute five, and what does it do at minute sixty?
- How do you tell 'the customer abandoned the cart' from 'the release message was lost', and which way do you fail?
Availability latency spikes in a sawtooth matching the cache TTL
Medium · cache stampede · single-flight · degradation
Available-to-promise reads are served from a cache with a fixed 60-second TTL, warmed when a drop starts. At peak the service takes 8,000 reads per second at a 99 percent hit rate. p99 is flat at 6 ms for most of each minute, then spikes above 900 ms, and origin request rate steps up roughly a hundredfold at the same instants, exactly 60 seconds apart. Explain the pattern with numbers, give two mechanisms that prevent it and what each costs, and state what this cache is and is not allowed to answer.
Approach
- Read the period first. A spike locked to exactly the TTL means entries were written together and therefore expire together; a spike at random intervals would be eviction under memory pressure, which has a different fix. Warming the cache at drop start is what synchronised them.
- Do the per-key arithmetic, because the aggregate rate hides the mechanism. By Little's Law the number of concurrent origin fetches for one key is its request rate times the origin latency: a hot variant at 1,000 reads per second with a 40 ms origin fetch produces about 40 simultaneous, identical fetches at each expiry. Multiply across the keys that expire in the same second and the origin sees roughly the full 8,000 rps it was built to be shielded from.
- Confirm rather than assume: the origin's statement count at a spike should be about the request rate times latency per hot key, and the duplicate fetches should be byte-identical results computed concurrently.
- Apply request coalescing per key so one fetch per key per expiry reaches the origin and the rest wait on it. Cost: every waiter's latency is now bounded by that single fetch, so it needs its own timeout and a defined answer when it fails, or one slow fetch stalls every reader of the hottest key.
- Apply TTL jitter independently, writing each entry with a TTL drawn from a band around the base, for example 60 seconds plus or minus 20 percent. Cost: nothing beyond a slightly wider staleness band, which is why it is the cheaper of the two and neither substitutes for the other — jitter spreads the misses, coalescing collapses the duplicates within one miss.
- Close with the correctness boundary, because it is the reason stale-while-revalidate is acceptable here at all. This cache is a hint: it may answer 'definitely none left' and shed the read, and it may never answer 'yes, those units are yours'. The binding decision is the conditional write against inventory_position judged by affected-row count, so a few seconds of optimistic staleness costs a failed reservation, not an oversell.
Follow-up
- A drop makes thousands of requests for variants that do not exist yet. What does negative caching need that positive caching does not?
- Coalescing makes one fetch serve 40 waiters. What is the correct behaviour for the other 39 when that fetch times out?
- If the cache is only ever a hint, what measurable harm does a 10-second staleness band actually do, and to which metric?
Suggested practice checklist
For a candidate senior enough that the loop turns on design and judgement rather than on whether the coding round gets finished. Five days build one system properly and then stress it; coding gets a single maintenance day, on the assumption that the risk at this level is an unexamined tradeoff rather than a missed algorithm.
Day 1 — Numbers before diagrams
- Build your own reference card of the figures you will re-derive all week: bytes for a realistic record, requests per second implied by a given daily active count, and the storage that a year at a given write rate produces. Derive each one rather than copying it, because the derivation is what survives a follow-up.
- Turn one product statement into capacity requirements. From ten million daily users at four writes and forty reads each, state the peak-to-average factor you are assuming and why, then produce peak write QPS, peak read QPS and a year of storage.
- Write the two numbers whose order of magnitude changes the design, the read-to-write ratio and the working-set size against memory per node, and state the threshold at which each one flips your answer.
Deliverable: A one-page numbers card and one worked capacity estimate with every assumption written down.
Day 2 — One system, from requirements to schema
- Spend the first ten minutes producing only functional requirements, non-functional targets with numbers attached, a p99 latency, a durability expectation, a consistency requirement, and an explicit out-of-scope list.
- Define the interface before the boxes: the three or four endpoints, their parameters, what each returns, and which of them are idempotent.
- Write the data model, then write the single access pattern that justifies it, and state what the schema would have to become if the dominant access pattern were the other one.
Deliverable: One design carried to endpoint-and-schema depth, with non-functional targets expressed as numbers and a written out-of-scope list.
Day 3 — The consistency you are actually buying
- Write out what a client sees under asynchronous replication when its write commits on the leader and its next read is served by a lagging follower, then write the two fixes, pinning that session's reads to the leader for a bounded window or carrying a version token the replica must reach, and the cost of each.
- Work the quorum arithmetic on paper for N of three with W and R of two, and separate what R + W > N does guarantee, that any read set intersects any write set, from what it does not: on its own it is not linearizability, and a sloppy quorum that accepts writes on nodes outside the preference list breaks even the intersection.
- Take two storage choices with different defaults, a single-leader relational store committing synchronously and a quorum-replicated store that converges eventually, and write the specific product behaviour that would be wrong under each, rather than a general statement about which is stronger.
Deliverable: A page separating what quorum overlap guarantees from what it does not, with one concrete product misbehaviour attached to each gap.
Day 4 — Failure is the design
- For one write path, work through the case where the client times out after the server has already committed, then design the idempotency key: who generates it, how long it is retained, and what the duplicate request returns.
- Express the retry policy as parameters rather than as a word: maximum attempts, base delay, backoff factor, jitter, and which error classes are retried at all. Then state why retrying a non-idempotent write without a key is a correctness bug and not merely waste.
- Compute the fan-out effect on tail latency. If a request waits on ten backends and each independently exceeds its p99 one percent of the time, the chance at least one is slow is 1 - 0.99^10, about ten percent. Then write why independence is the optimistic assumption and what correlates them in practice.
- Name the backpressure mechanism for one queue or one dependency in the design, a bounded queue with shedding or a concurrency limit, and write what the caller is told when it engages.
Deliverable: One write path with an idempotency design, a parameterised retry policy, and a written tail-latency calculation with its assumption named.
Day 5 — Scaling the hot path
- Choose cache-aside or write-through for one read path and write the staleness window each produces, then name the invalidation event and what the system does when that event is lost.
- Design against the stampede: either coalesce requests so only one recomputes a missing key, or refresh early with jittered expiry, and write why identical TTLs on keys populated in the same moment produce a synchronised expiry and a thundering herd.
- Shard one table by a key you choose, then answer the two questions that break the choice: which queries now require a scatter-gather, and what happens to the distribution when one tenant is a hundred times larger than the median.
- Write the cost of adding a node under plain modulo placement, where nearly every key moves, against consistent hashing, where roughly one key in n+1 moves, and state what virtual nodes are for.
Deliverable: A caching and sharding decision for one path, each with its failure mode and its rebalancing cost written beside it.
Day 6 — Keep the coding hand in, at the bar that applies to you
- Solve one medium problem in thirty minutes, then spend twenty more making it production-shaped: named invariants, validation at the boundary, and errors that distinguish a caller mistake from an internal fault.
- Write the tests you would require of a colleague's version of that function: one for empty input, one for the boundary, and one for the case the implementation is most likely to get wrong.
- Read a piece of your own code from six months ago and write the change you would ask for, phrased as you would actually phrase it in review.
Deliverable: One problem hardened to review standard, with its test list and one written review comment.
Day 7 — Defend it while being interrupted
- Run a forty-five-minute design mock with an interviewer briefed to change a requirement halfway, a tenfold traffic increase or a new strict consistency requirement, and to push on one number you estimated.
- Rehearse the two sentences a senior loop is listening for: naming the tradeoff you are choosing against and why, and saying what you would measure to learn that the choice was wrong.
- Prepare the design you regret: a real decision, the constraint that produced it, what it cost, and what you changed afterwards.
Deliverable: Mock notes recording how the design changed under the new requirement, plus a written account of one regretted decision.
The estimate and the day you knew
A slipped date is only a bad story if you sat on it. What matters is what you believed when you gave the number, the signal that told you it was wrong, how many days passed before you said so, and what you cut rather than asking for more time. Scope you defended counts as much as scope you dropped.
What is your experience with React and Redux state management?
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
How do you handle component lifecycle and performance optimization in React?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- 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
- What would you do differently if you ran that again?
- What did you decide not to do, and why?
How do you manage your workload when balancing multiple client requirements?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What did you decide not to do, and why?
- What would you do differently if you ran that again?
How do you handle technical disagreements within a team setting?
Medium · behavioural and engineering judgement
Approach
- State the situation in two sentences and spend the rest on the reasoning.
- Close with what you would do differently, concretely.
- 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?