As a Software Engineer at Etsy, you will build and scale systems that power a global e-commerce marketplace connecting tens of millions of buyers with millions of creative sellers. Engineering at Etsy directly drives the core user experience, from high-concurrency search engines, seller inventory management, and real-time messaging, to machine learning infrastructure, personalized recommendations, and checkout pipelines.
The engineering organization operates at significant technical scale, handling millions of requests per minute across a distributed microservices and web architecture built on JavaScript/Node.js, Python, PHP, and modern frontend tools like React. Engineers are expected to balance raw system scalability with user-facing product value, taking end-to-end ownership of services from design and implementation through deployment, monitoring, and iterative improvement.
What makes the Software Engineer position distinct at Etsy is its heavy emphasis on practical, real-world software engineering over abstract computer science puzzles. You will work on production systems where code quality, observability, database optimization, and cross-functional alignment with product managers and designers are evaluated just as rigorously as system capacity and algorithmic efficiency.
Phone Screen
reportedBefore anything technical happens, someone has to decide which rung of the ladder your loop is calibrated to, and that decision sets the bar for every round after it. It comes from how you describe scope, not from your title, because titles do not convert cleanly between companies. The weak version of the answer is team size and years. The strong version names the largest change you shipped where nobody reviewed the design, what would have broken if you had been wrong, and what you were paged for. Get the level said out loud on this call, because the range and the loop both follow from it.
What to demonstrate
- Whether the scope in your own account maps onto a level the team actually has an opening at, so a mismatch ends the process cheaply rather than after four interviewers have spent a day
- Whether your title needs re-mapping: the same word describes very different amounts of independent decision-making at a twenty-person company and a ten-thousand-person one
- Whether your compensation expectation can be filled at that level in the structure the role pays in, which is why the number gets asked for before any engineer is scheduled
How to prepare
- Write down two changes from the last two years: the largest one you designed with nobody reviewing the design, and the largest one where someone more senior did. Lead with the first when scope comes up, and be ready to say which parts of the second were yours
- Ask which level the loop is calibrated to and what changes at the level above it, then plan your weeks from that answer rather than from the posting
- Settle a total-compensation range beforehand with the split named, base against bonus against equity and its vesting period, so a question about numbers gets a number instead of the word market
Technical Interview
reportedInput bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.
What to demonstrate
- Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
- Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
- Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
- Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply
How to prepare
- For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
- For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
- Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last
Onsite Interviews
reportedA day like this is several different games in a row, and the expensive mistake is carrying the previous one into the next room. Coding rewards narrow precision and finishing inside a timer. Design rewards breadth, stated assumptions and naming what you are deliberately not building. Behavioural rewards specificity about people and decisions. Candidates who over-engineer a coding problem they were supposed to finish, or who start sketching class hierarchies before anyone has agreed what the system has to do, are usually still playing the last round. Between rooms, name out loud which game the next one is.
What to demonstrate
- Whether the coding round ends with something that runs and has been traced against a degenerate input, rather than an extensible design that was never finished
- Whether a design discussion opens by agreeing on traffic shape, read-to-write ratio and what is allowed to be stale, instead of proceeding from an architecture you arrived with
- Whether a behavioural answer names a person, a disagreement and what you did about it, rather than describing the system the story happened inside
- Whether the opening habits still appear late in the day: restating the problem, asking for constraints, saying the plan before typing
How to prepare
- Book three mocks of different types back to back on one afternoon and ask each interviewer afterwards which round you answered in the wrong mode
- Write a three-line opening script per round type — coding: restate, name the approach and its cost, then type; design: ask for scale, read-write mix and what must not break; behavioural: name the person, the stakes and the decision — and run it off a card so the switch is mechanical rather than remembered
- Practise coding with a timer you do not extend, stopping when it stops, so the trained reflex is to finish a correct solution rather than to keep improving one
2 candidate reports. Individual accounts describe a particular role and hiring cycle.
Etsy Machine Learning Engineer Interview Experience — A Recruiter Call That Felt AI-Scripted
View report detailsEtsy Data Scientist Interview Experience — Research Talk Plus a Live Autocomplete Coding Question
View report detailsPracHub editorial advice for the preparation topics above.
Treating the provider's device as the source of truth for state
The app is offline part of the time and buffers transitions, so events arrive late, out of order, and occasionally duplicated after a retry. A status column written last-write-wins will therefore accept 'started' after 'completed' and resurrect a cancelled booking, which re-enables charging on it. Client timestamps make it worse because the device clock is user-settable and drifts, so ordering by them is not ordering at all. The server must assign the ordering (a per-aggregate monotonic sequence) and every transition must be a guarded write against the expected current state, with the client event treated as a request to transition rather than as the transition.
Filtering providers with a bounding box or a distance function over raw lat/lon columns
A WHERE clause computing great-circle distance per row cannot use a B-tree index and degenerates to a scan of every provider in the table, which is fine at 5k providers in staging and falls over at 100k in a dense market at peak. A lat/lon bounding box is index-assisted but returns a square that over-selects badly near the poles and still needs a second-pass distance filter. The working answers are a spatial index — PostGIS GiST with ST_DWithin (which is index-assisted, unlike ST_Distance in a predicate) or the KNN <-> operator for ordered nearest-neighbour — or cell-based bucketing in a key-value store. Cell bucketing has its own edge: two points metres apart can sit in different cells, so a prefix-only lookup silently misses the closest provider unless the query also covers the eight neighbouring cells.
Answering a debugging question with a guess instead of a bisection
Give a procedure that halves the search space at each step: confirm the symptom reproduces, establish the last known-good version, input or timestamp, then bisect over commits, over the data, or over the layers of the request path. A plausible cause with no way to confirm it is the same move whether it happens to be right or wrong, which is why it scores nothing.
Arguing past a hint
When the interviewer asks what happens for a particular input or floats a different data structure, stop and take it seriously; it is almost always a correction rather than idle curiosity. Talking over it converts a recoverable wrong turn into a data point about how you handle review.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Enumerate the eight neighbours of a geohash cell
Provider presence is bucketed by a 7-character geohash over the base-32 alphabet 0123456789bcdefghjkmnpqrstuvwxyz, five bits per character, with longitude taking the first bit and every other bit thereafter. Write a function that takes a geohash-7 string and returns its neighbouring geohash-7 cells. Handle the antimeridian and the poles explicitly rather than by accident. State the time and space complexity, what a geohash-7 cell measures on the ground, and how that measurement changes with latitude.
Approach
- Decode the string: map each character to its 5-bit value through a lookup table or an index into the alphabet, remembering that a, i, l and o are absent, so a naive ASCII offset is wrong. Seven characters give 35 bits.
- De-interleave those 35 bits. Bit 0 is longitude and the parity alternates, so longitude takes the 18 even positions and latitude the 17 odd ones, giving a grid of 2^18 columns by 2^17 rows.
- The neighbours are (lat_index + dy, lon_index + dx) for the eight non-zero (dx, dy) pairs. Longitude wraps: take lon_index modulo 2^18, so a cell at +180 degrees is genuinely adjacent to one at -180. Latitude clamps: a cell in the top or bottom row has no neighbour beyond it and returns fewer than eight. Wrapping latitude the same way teleports across the pole to an unrelated longitude.
- Re-interleave and re-encode in 5-bit groups. The whole operation is O(L) time and O(1) space for L = 7 characters, and is pure integer arithmetic — no trigonometry is involved in finding neighbours.
- Ground size: a geohash-7 cell is roughly 153 m east-west at the equator (360 degrees / 2^18 = 0.001373 degrees, times about 111,320 m per degree) and roughly 152 m north-south (180 degrees / 2^17, times about 110,574 m per degree). The north-south extent is essentially constant; the east-west extent scales with cos(latitude), so at 60 degrees the cell is about 76 m wide. A ring count must therefore be derived from the radius in metres at the query's latitude, not fixed once from an equatorial figure.
- State why neighbours are needed at all: two providers ten metres apart can sit in different cells when they straddle a boundary, so a prefix-only lookup silently misses the nearest provider. Querying the 3x3 block guarantees that any provider within the cell's smaller dimension of the query point is in the candidate set.
Follow-up
- How many rings do you need for a 2 km radius at latitude 55, and how do you compute that rather than guess?
- What does the string-prefix property buy you here that an S2 or H3 cell id does not, and what does it cost?
- The 3x3 block returns 3,000 providers at peak — what do you do before computing any distances?
Detect unbalanced ledger transactions in a single pass
You are given up to 50 million ledger_entry rows streamed in arbitrary order: entry_id, transaction_id, account_id, direction ('debit' or 'credit'), amount_cents (a positive bigint), currency, and idempotency_key. The invariant is that the signed postings of every transaction_id sum to zero. Report every transaction that violates it, together with its residual in cents. Duplicate rows sharing an idempotency_key may appear because a consumer retried a batch. Target one pass, with memory proportional to the transactions currently open rather than to the input length.
Approach
- Key the accumulator on (transaction_id, currency), not transaction_id alone. Balance is a per-currency property; bucketing across currencies lets a 4000-cent USD leg and a 4000-cent EUR leg cancel into a false pass. A genuinely multi-currency event balances through an FX clearing account with two balanced halves.
- Fix a sign convention once — debit +1, credit -1 — and accumulate in signed 64-bit integer cents. Never use a float: a double represents only integers below 2^53 exactly, and money comparisons against zero must be exact, not within an epsilon.
- Handle duplicates by scope. If the source is a table scan, the UNIQUE constraint on idempotency_key means duplicates cannot exist and no dedupe is needed. If the source is a replayed event stream, keep the seen idempotency_keys only for transactions still open and free them when the transaction is emitted, so dedupe memory stays O(open) rather than O(n).
- At end of stream emit every bucket whose residual is non-zero. Time O(n), space O(d) where d is the number of distinct open (transaction_id, currency) pairs — which for a well-behaved feed is small because postings of one event are written together.
- If the source can be ordered or hash-partitioned by transaction_id, memory drops to O(1) per group and the job parallelises cleanly: hashing on transaction_id guarantees every posting of one event lands in the same worker, so no worker ever sees a partial transaction.
Worked solution 20 min
- Take four transactions. T1 capture: debit consumer_receivable 4000, credit provider_payable 3200, credit platform_revenue 800. T2 partial refund: credit consumer_receivable 1000, debit provider_payable 800, debit platform_revenue 200. T3 incentive: debit promotions_expense 500, credit provider_payable 500. T4 capture: debit consumer_receivable 2500, credit provider_payable 2000, credit platform_revenue 400.
- Append a duplicate of one T1 posting with the same idempotency_key, placed at the end of the stream, to exercise the dedupe path.
- Run the accumulator with debit = +1 and credit = -1: T1 gives +4000 -3200 -800 = 0; T2 gives -1000 +800 +200 = 0; T3 gives +500 -500 = 0; T4 gives +2500 -2000 -400 = +100.
- Shuffle the input and re-run to confirm the result is order-independent.
- Drop the duplicate row and re-run to confirm the output is unchanged.
Follow-up
- How do you catch a transaction that sums to zero but has the sign flipped on both legs, so money moved the wrong direction?
- This must run nightly over a growing table without a full scan — how do you make it incremental when a chargeback can be posted today with an effective_at from three weeks ago?
- What would you change if a single economic event legitimately spans two currencies?
Reorder a duplicated event stream under a memory cap
A consumer reads outbox_event rows from an at-least-once bus: aggregate_type, aggregate_id, sequence_no, event_type, payload, where sequence_no is monotonic and gapless per aggregate. Delivery is out of order and duplicated, and the consumer may restart at any point. Apply each aggregate's events in sequence order, once each in effect, with total buffered events capped at 100,000 across all aggregates. Describe the data structures, the restart behaviour, and what you do when a gap does not close.
Approach
- Per aggregate keep next_expected plus a small map from sequence_no to the buffered event. Initialise next_expected from a durable checkpoint, never from 1: a consumer that starts mid-stream and assumes 1 will stall forever waiting for events the bus will not resend.
- Dispatch on the arriving sequence_no. Below next_expected is a duplicate or a replay and is dropped. Equal to next_expected is applied, then increment and drain the pending map while it contains the new next_expected. Above next_expected is buffered. Because you only ever probe the exact successor, the pending map is a hash map with O(1) amortised work per event; a heap would give O(log b) and buy nothing, since the minimum is never the question.
- Do not claim exactly-once over an at-least-once transport; what you build is idempotent application. The effect and the advance of next_expected must commit in one local transaction, or a crash between them re-applies on restart. Checkpointing every N events instead is a deliberate decision to re-apply up to N events, and is only safe when the effect is itself idempotent.
- Enforce the cap by counting buffered events globally. On reaching 100,000, stop acknowledging and let backpressure reach the bus rather than evicting, because an evicted event is lost permanently under a transport that will not redeliver what you acked. Where there is no backpressure, evict the aggregate holding the oldest buffered event and re-read its events straight from outbox_event by (aggregate_type, aggregate_id, sequence_no), which is unique and therefore an exact lookup.
- Resolve stuck gaps by reading the source, not by timing out. After a timeout, look up the missing sequence_no in outbox_event: if it is absent there, it was never written and skipping is correct; if it is present, the relay dropped it, so apply it and alert on the relay. Never advance next_expected over an unresolved gap on a timer alone.
- Summarise the bounds: O(1) amortised work per event, memory proportional to how far ahead the transport is allowed to run rather than to stream length. Per-aggregate ordering is the only ordering the bus offers, through partitioning on aggregate_id; there is no global order to reconstruct, and attempting one serialises the whole consumer.
Follow-up
- One aggregate produces 60 percent of the buffered events — how do you keep it from starving every other aggregate out of the cap?
- The relay is re-publishing a week of events after a bug; how does the consumer survive that without a stampede against the source table?
- The effect is a call to a third-party API rather than a local write, so there is no shared transaction — what replaces the same-transaction dedupe?
Decide whether provider earnings belongs on the booking row
The provider app shows a weekly earnings screen: one row per completed booking with its net payout, plus a week total. It currently sums ledger_entry restricted to that provider's provider_payable account. A proposal adds booking.net_payout_cents, written in the same transaction as the capture, and provider.lifetime_earnings_cents, incremented alongside it. A typical provider completes 40 to 200 bookings a week. Decide what to store and what to derive, and give the DDL and the read query that back your answer.
Approach
- Size the read before optimising it. 40 to 200 bookings at roughly four postings each is under a thousand rows; with a btree on
ledger_entry (account_id, effective_at)that is one index range scan, single-digit milliseconds. There is no read problem on this screen, so the denormalisation buys nothing here and must justify itself somewhere else or not at all. - Price the mutable counter's write cost:
provider.lifetime_earnings_centsmakes every capture for a provider contend on one row, serialising that provider's concurrent writes behind a row lock and adding a deadlock edge with any other transaction that touchesproviderin a different order. - Price its correctness cost, which is the decisive one: a refund lands days later and a chargeback weeks later, each as a new posting whose
effective_atfalls inside the original period. A counter incremented at capture is then permanently wrong with no record of the divergence, while the ledger can be re-summed for any period indefinitely. - If a rollup is genuinely needed — a payout batch over every provider, or a year-to-date view spanning 50,000 bookings — build it as a derived table keyed
(provider_id, period_start, currency)that is rebuilt by re-summing the ledger, never incremented, with acomputed_throughtimestamp so a late posting marks the period stale rather than silently diverging. Make it idempotent per(provider_id, period_start)with a unique constraint so a partial failure is re-runnable. - State the rule you applied: denormalise a derivable value only when you can rebuild it from the source of truth on demand. The moment the copy is the only copy, late-arriving events make history irreproducible, and money is the domain where late events are guaranteed.
Follow-up
- The payout batch must be idempotent per
(provider_id, period). Give the DDL that makes paying twice impossible rather than merely unlikely. - A chargeback arrives with
effective_atinside a period that has already been paid out. Does the rollup for that period change, and what does the provider see on the screen? - What index serves the read query, and what does the same query cost for a provider asking for year-to-date after 50,000 bookings?
Fix the double-counted totals in a two-child-table join
booking has two children: booking_adjustment(adjustment_id, booking_id, kind, amount_cents) and ledger_entry(entry_id, booking_id, account_id, direction, amount_cents, effective_at). Finance runs one query that joins booking to both, groups by booking_id, and sums each. For a booking with 3 adjustments and 4 postings the adjustment total comes out four times too large. Explain the mechanism, then write a correct query returning one row per booking completed yesterday with its adjustments total and the net amount owed to that provider. Say when you would use LATERAL and when a pre-aggregated CTE.
Approach
- Name the mechanism: joining two independent one-to-many children produces their cross product per parent, so 3 adjustments and 4 postings yield 12 rows and each child's aggregate is multiplied by the other child's cardinality. The fingerprint is that the wrong total is an exact integer multiple of the right one, which is why it survives code review — it looks like a number, not like an error.
- Reject the plausible patch.
COUNT(DISTINCT entry_id)is correct because the id is unique, butSUM(DISTINCT amount_cents)collapses two genuinely distinct 500-cent adjustments into one and is wrong in a way that only shows up when amounts repeat. - Aggregate each child independently.
LEFT JOIN LATERAL (SELECT SUM(ba.amount_cents) AS adjustments_cents FROM booking_adjustment ba WHERE ba.booking_id = b.booking_id) a ON TRUE, and a second lateral for the ledger, is two index probes per booking and suits a small outer set such as one day. Pre-aggregated CTEs grouped bybooking_idscan each child once and hash-aggregate, which wins as the outer set grows; pick by outer cardinality and measure the crossover rather than asserting one. - Add the filter that is not optional and is easy to omit: every
transaction_idbalances, so summing all postings for a booking returns exactly zero. The provider figure must restrict to that provider'sprovider_payableaccount — resolved by joiningaccounton the booking'sprovider_id— and must sign by direction withSUM(CASE WHEN direction = 'credit' THEN amount_cents ELSE -amount_cents END). - Choose the date column deliberately.
booking.completed_atbounds which bookings appear, but a refund'seffective_atcan fall inside yesterday while itsposted_atis next week; state which column the report keys on, because re-running it tomorrow will return a different number under one choice and the same number under the other.
Worked solution 25 min
- Insert one booking with 3 adjustments and 4 postings, run the naive query, and confirm the multiples are exactly 4x and 3x.
- Rewrite with two lateral subqueries and re-run against the same data.
- Drop the
account_idfilter from the ledger subquery and observe what the net becomes. EXPLAIN (ANALYZE)both the lateral and the CTE form over a one-day window and over a 90-day window.
Follow-up
- Rewrite it with pre-aggregated CTEs and say at what outer cardinality you would switch, and how you would measure the crossover rather than guess it.
- How would you catch this class of bug automatically in a reporting test suite, given that the wrong answer is a plausible-looking number rather than an exception?
- The same booking has a refund with
effective_atin yesterday's window andposted_atnext week. Which does this report key on, and what does the other choice change?
Architect a photo upload, image processing, and globally distributed s…
Architect a photo upload, image processing, and globally distributed serving infrastructure capable of handling high volumes of product images.
Approach
- Name the read and write paths separately; they rarely have the same bottleneck.
- Choose a partition key and say what query it makes expensive.
- State the consistency you need, and where you are willing to be stale.
Follow-up
- What would you drop to keep the system up under load?
- What breaks first when traffic grows ten times?
Design a high-availability real-time messaging system to enable seamle…
Design a high-availability real-time messaging system to enable seamless buyer-seller communication across web and mobile applications.
Approach
- Choose a partition key and say what query it makes expensive.
- 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.
Follow-up
- How does this behave when that dependency is down for an hour?
- What would you drop to keep the system up under load?
Ingesting out-of-order lifecycle events from mobile clients
The provider app posts lifecycle transitions (en_route, arrived, in_progress, completed, cancelled), buffering them while offline and flushing on reconnect, with a device clock the user can set. booking holds (booking_id, status, version INT, started_at, completed_at, cancelled_at, cancelled_by). In production you see in_progress arriving after completed, and duplicate flushes of the same transition. Design the ingest path so the state machine can never move backwards or leave a terminal state and a duplicate flush has no effect. State what the client event is allowed to determine and what it is not.
Approach
- Declare the transition graph as data rather than as scattered conditionals: an explicit set of allowed (from, to) pairs with completed, cancelled and disputed terminal, so an illegal transition is rejected by a lookup and answered with a typed conflict instead of reaching a write at all.
- Let the server own ordering. The client event is a request to transition, not the transition; stamp your own monotonic per-booking sequence on arrival and never order by the device timestamp, which is user-settable and drifts. Keep the client timestamp in the payload as evidence for support, and clamp it into [accepted_at, now()] before it is allowed to influence anything billable such as wait time.
- Make the write guarded on two independent things: UPDATE booking SET status = $new, version = version + 1, = $server_now WHERE booking_id = $1 AND status = $expected AND version = $expected_version. The status predicate rejects illegal and late transitions; the version check rejects two legal transitions racing, such as a support agent and the app acting at once. Zero rows means re-read and decide: already at the target state is a duplicate and returns success, anything else is a rejection.
- Dedupe on (booking_id, client_event_id) with a unique index, and insert that row in the same transaction as the state change. Committing the dedupe record separately does not remove the double-apply, it just relocates it to the crash window between the two commits.
- The out-of-order case then needs no special code: in_progress arriving after completed expects status 'arrived', finds 'completed', affects zero rows and is discarded. Count the discards, because a rising discard rate after a client release is a client bug signal rather than background noise.
Worked solution 30 min
- Write the allowed (from, to) pairs as a table, marking terminal states and which transitions move money.
- Write the guarded UPDATE with both the status predicate and the version check, and say what each protects against on its own.
- Write the dedupe table and draw the transaction boundary that contains both it and the state change.
- Trace a reconnecting client's flush of [in_progress, completed, in_progress] through the handler and give the final row plus the per-event outcome.
Follow-up
- Support must cancel a booking the provider already marked completed. Where does that sit in your graph, and which column records who did it?
- Both apps report arrival. Which one moves the state, and what happens to the other event?
- Your discard counter jumps tenfold after a client release. What is the first query you run?
Modify an array-manipulation or string-parsing function step-by-step a…
Modify an array-manipulation or string-parsing function step-by-step as new feature requirements and constraint changes are introduced.
Approach
- Say what evidence would prove you wrong, then go and look for it.
- Separate the trigger from the cause; the deploy is rarely the bug.
- Check the instrumentation before believing the symptom.
Follow-up
- How would you tell a cause from a coincidence here?
- What would you look at first, and what would it rule out?
Implement a priority queue data structure from scratch, complete with …
Implement a priority queue data structure from scratch, complete with custom methods, optimal time complexity, and comprehensive edge-case handling.
Approach
- Say what evidence would prove you wrong, then go and look for it.
- Separate the trigger from the cause; the deploy is rarely the bug.
- Pick a bisection that eliminates candidates whichever way it turns out.
Follow-up
- What would you look at first, and what would it rule out?
- How would you tell a cause from a coincidence here?
Build a live web application component using React or vanilla JavaScri…
Build a live web application component using React or vanilla JavaScript that consumes an external REST API and dynamically renders data in the DOM.
Approach
- Establish what changed and when, before forming any theory.
- Check the instrumentation before believing the symptom.
- Say what evidence would prove you wrong, then go and look for it.
Follow-up
- What would you add now so this is faster to diagnose next time?
- What would you look at first, and what would it rule out?
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Practice prompt ↗Practice prompt ↗03Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Practice prompt ↗Practice prompt ↗04Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Practice prompt ↗Practice prompt ↗06One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Practice prompt ↗Practice prompt ↗07Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.
Practice prompt ↗Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
Counting review comments or mentees proves nothing. The useful version is a specific change you approved with a reservation you stated, or one you blocked and the delay that cost. Say which standard you were holding and why it was worth the friction. A mentoring story needs the thing the other person can now do without you.
Unblocking a stuck engineer without taking the keyboard
Describe a time you unblocked someone who had been stuck for more than a day. Say how you learned they were stuck, what you diagnosed the real blocker to be - missing context, a wrong mental model, a genuinely hard bug, or reluctance to ask - and what you actually did. State explicitly whether you took the keyboard and what that cost. Then say what changed so the next person is not stuck in the same place: a document, a test, a renamed function, a constraint that turns the mistake into an error.
Approach
- Diagnose the blocker type first, because the responses diverge sharply. Missing context is a five-minute fix; a wrong mental model has to be corrected out loud and checked; reluctance to ask is a team-norm problem you cannot solve in one sitting and should not pretend you did.
- Describe the intervention at the level of what you said or drew - the interleaving you sketched, the question that surfaced their hidden assumption - rather than the word 'pairing', which conveys nothing about what you contributed.
- Be explicit about taking the keyboard. It is sometimes right under time pressure and it always trades their learning for your speed. Naming the trade is the difference between mentoring and rescuing.
- Verify the unblock rather than assuming it: did they finish it alone, did they hit the same wall a fortnight later, did they later explain it to someone else. That last one is the strongest available evidence.
- Name the durable artifact. A mentoring answer with no residue describes one act of help; the leverage is in the test, the comment, or the constraint that makes the same confusion impossible next time.
Follow-up
- How long did you let them struggle before stepping in, and how did you choose that duration?
- What did you get wrong about why they were stuck?
- When is taking the keyboard the correct call rather than the easy one?
Disclosing a money bug nobody has complained about yet
You discover that a retry path has been double-capturing: rows in payment_attempt with status 'unknown' were retried with a freshly generated idempotency_key instead of the stored one, so some bookings carry two successful captures. No customer has complained. You have the ledger and the gateway's own records. Describe what you do, in what order, and whom you tell. State what you stop first, how you bound the affected set exactly, how corrections reach ledger_entry, and what you say to your manager if asked to hold the disclosure until the quarter closes.
Approach
- Stop the bleeding before measuring it. Every hour spent investigating with the retry path live adds rows to the set you will later have to refund, so the first action is a flag, a config change, or a revert - whichever reaches production fastest.
- Bound the set exactly rather than estimating. Group successful captures by booking_id having count(*) > 1, then confirm each candidate against the gateway's record, since the local table is the artefact you already know to be wrong. Produce a list of booking ids and a total amount, not a rate.
- Escalate in writing, immediately, with the number attached and a next-update time. A money defect has owners outside engineering, and the cost of support or finance hearing it from a cardholder instead of from you is out of all proportion to the delay you saved.
- Remediate with postings, never edits: one compensating refund posting per duplicated capture, idempotent per booking and duplicate attempt, leaving the original rows untouched so every historical report stays reproducible. Confirm each transaction_id still sums to zero.
- Close the class rather than the instance. Derive the idempotency key from (booking_id, kind, logical_attempt) and persist it before the call so a retry replays it byte-identically; add the reconciliation pass that resolves 'unknown' against the gateway instead of guessing; add an alert that fires on any booking with more than one succeeded capture.
- On the request to wait: say that you will not delay notification, offer to sequence the remediation around the close, and put the disagreement in writing. The refusal is quiet and specific, not a stand on principle.
Follow-up
- The duplicate window overlaps a payout batch that already paid providers on the inflated amounts. What now?
- How do you choose between proactively refunding everyone affected and waiting for disputes to arrive?
- The gateway's record disagrees with your ledger for eleven bookings. Which one do you trust, and what do you do about the rest?
Walk an outage you owned from first signal to permanent fix
Pick an incident where you were the primary owner, not a helper. Tell it as a timeline: the first signal and what threshold fired it, what you ruled out and how, the mitigation you shipped and how long it took to reach production, and the permanent fix that followed. Quantify the blast radius in a countable unit - bookings affected, duplicate captures, minutes of degraded dispatch - and say how you bounded that number instead of extrapolating it. Close with the one change that made the class of failure impossible rather than merely unlikely.
Approach
- Lead with impact in one sentence - who was affected, for how long, in what unit - before any chronology, because that sentence is what the listener calibrates seniority against.
- Give the detection path honestly. 'A consumer emailed support' and 'the duplicate-capture alert fired at 14 per minute against a baseline of zero' describe very different systems, and claiming the second when it was the first collapses on the first follow-up.
- Run two clocks: time to mitigate and time to fix. Mitigation is whatever stops the bleeding within minutes (a flag, a rate cap, draining one partition); the fix is the structural change that lands days later. Collapsing them into one story hides whether you can triage under pressure.
- Bound the affected set with a query you can state, not a rate times a duration - for example successful captures grouped by booking_id having count(*) > 1 across the incident window, cross-checked against the gateway's own record. An exact list survives scrutiny; an estimate invites it.
- End on the structural change and say what it does not catch. A partial unique index on the live-offer status, an EXCLUDE constraint on overlapping reservations, or a guarded UPDATE whose affected-row count decides the winner each convert a silent corruption into a loud error, and each has a boundary worth naming.
Follow-up
- What did you believe mid-incident that turned out to be wrong, and what made you drop it?
- How did you verify impact had stopped, without relying on the alert clearing?
- Who else on the team could have shipped the same bug that quarter, and what stops them now?
- 01
Describe a time you unblocked someone who had been stuck for more than a day. Say how you learned they were stuck, what you diagnosed the real blocker to be - missing context, a wrong mental model, a genuinely hard bug, or reluctance to ask - and what you actually did. State explicitly whether you took the keyboard and what that cost. Then say what changed so the next person is not stuck in the same place: a document, a test, a renamed function, a constraint that turns the mistake into an error.
- 02
You discover that a retry path has been double-capturing: rows in payment_attempt with status 'unknown' were retried with a freshly generated idempotency_key instead of the stored one, so some bookings carry two successful captures. No customer has complained. You have the ledger and the gateway's own records. Describe what you do, in what order, and whom you tell. State what you stop first, how you bound the affected set exactly, how corrections reach ledger_entry, and what you say to your manager if asked to hold the disclosure until the quarter closes.
- 03
Pick an incident where you were the primary owner, not a helper. Tell it as a timeline: the first signal and what threshold fired it, what you ruled out and how, the mitigation you shipped and how long it took to reach production, and the permanent fix that followed. Quantify the blast radius in a countable unit - bookings affected, duplicate captures, minutes of degraded dispatch - and say how you bounded that number instead of extrapolating it. Close with the one change that made the class of failure impossible rather than merely unlikely.
Is this an official Etsy interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Etsy. Rounds and questions reflect what candidates have reported, not a process Etsy has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗What makes the Etsy technical interview process unique compared to other tech companies?
The process heavily emphasizes practical, real-world software engineering over theoretical puzzle-solving. Instead of traditional algorithmic LeetCode grinders, you will spend significant time debugging existing web applications in frameworks like Flask or Express.js, fixing broken test suites, and discussing real system architectures.
PracHub interview research ↗Can I complete the coding and debugging assessments in my language of choice?
While Etsy promotes a "language-agnostic" interview policy in theory, practical debugging screens and work samples frequently utilize standard web technologies such as JavaScript/Node.js, Python/Flask, or PHP. Clarify the exact environment with your recruiter beforehand to ensure you are comfortable navigating the provided IDE and stack syntax.
PracHub interview research ↗How important are the behavioral and cross-functional interview rounds?
Behavioral rounds are evaluated with equal weight alongside technical rounds. Etsy places strong cultural value on low-ego collaboration, clear communication with product managers and designers, and constructive problem-solving. Be prepared to share detailed, structured examples of past teamwork using the STAR format.
PracHub interview research ↗What is the typical timeframe for the complete interview process?
The entire process typically spans 3 to 5 weeks from initial recruiter outreach to final decision. Timeframes can vary based on team scheduling, holiday periods, or team-matching phases following successful technical loops.
PracHub interview research ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
platform · Accessed 2026-09-24 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
platform · Accessed 2026-09-24 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
platform · Accessed 2026-09-24