Thumbtack is a two-sided marketplace for local services that connects customers with local service professionals (pros) across many categories, from home remodeling to personal training. Software Engineers work on the systems behind that exchange: matching customer requests with available pros, search and discovery, messaging, and booking and transaction flows, across backend services, search ranking, web and mobile.
The interview questions candidates report follow the same domain. Coding prompts include flattening a nested list with an iterator, building and traversing a service-category tree, parsing search queries into structured filters, and routing a pro between several job sites. The practical design prompts are a calendar with no overlapping events for a pro, a key-value store with BEGIN, COMMIT and ROLLBACK, and a rolling-window rate limiter. The system design prompts are real-time lead matching, a notification service for push, SMS and email with guaranteed delivery and rate limiting, and a pro search system that weighs proximity, availability and ratings.
Plan for code that runs. Candidates describe writing code in a shared editor that has to compile and pass test cases in the phone screens and the onsite coding rounds, not pseudocode. Candidates often report that the technical assessment is an in-memory database with nested transactions. Treat that submission as code someone will review: keep it readable, modular, tested and object-oriented. Prepare for correct, tested and readable code first, then work on algorithm speed.
Recruiter Call
reportedCandidates describe a first call with a recruiter about your background, career goals and team alignment. It is also your best chance to learn how the rest of the loop is set up. Candidates report that the next stage is either an online assessment or a take-home challenge, and that the phone-screen stage is one or two interviews. Ask which applies to you, which language and environment you will code in, and whether design comes up before the onsite. The answers tell you whether to spend your first week on the in-memory database task, timed algorithm practice or design.
What to demonstrate
- Whether you can summarise your background briefly and link it to the kind of team you want, since team alignment is one of the reported topics of this call
- Whether your career goals are concrete enough for the recruiter to match you to a team, rather than a general wish to grow
- Whether you ask about the format of the next stages instead of guessing
How to prepare
- Write a short spoken summary of your background that ends on the kind of systems you want to work on next, such as marketplace matching, search, messaging or payments
- Prepare questions: online assessment or take-home, which coding platform and language, one or two phone screens, and where system design comes in
- Write down your constraints (start date, location, any competing timeline) so you can state them plainly if asked
Technical Assessment
reportedCandidates report an online technical assessment or a take-home coding challenge, and a recurring version of it is the in-memory database task. You parse and run commands such as GET, SET, UNSET and NUMEQUALTO, and support nested transaction blocks with BEGIN, COMMIT and ROLLBACK. It can be timed or take-home. Treat the submission as code someone will review: keep it readable, modular, tested and object-oriented. Output that passes the samples but sits in one long function is a weak submission. Build the design so that rollback does not copy the whole store and NUMEQUALTO does not scan every key.
What to demonstrate
- Correct behavior of nested transactions: a ROLLBACK undoes only the innermost open block, and values that were absent before the block become absent again
- Separation between command parsing, the storage layer and transaction bookkeeping, so each can be read and tested on its own
- Tests you wrote yourself, covering nested rollback, commit, rollback with no open transaction, and NUMEQUALTO after unsets
- Efficiency: rollback costs are proportional to the keys changed in the block, and value counts are maintained incrementally
How to prepare
- Implement the in-memory database from scratch once, in the language you will use, with a stack of per-transaction undo logs that record each key's prior value, including 'was absent'
- Maintain a value-to-count map updated on every SET, UNSET and rollback so NUMEQUALTO is a lookup, then write a test that proves the count survives a nested rollback
- Decide and document what COMMIT and ROLLBACK do when no transaction is open, and what COMMIT does with nested blocks, then test exactly that
- If it is a take-home, add a short README with how to run it, the assumptions you made and the complexity of each command
Technical Phone Screens
reportedCandidates describe one or two technical phone screens with senior engineers. Prepare to write code in a shared editor (CoderPad and HackerRank are named as examples) that compiles and runs against test cases. Reported coding prompts include a nested-list iterator, building and traversing a category tree from parent-child pairs, parsing search queries into structured filters, and graph routing between job sites. Those reports do not say which of them come up in the screens rather than the onsite. Ask clarifying questions, discuss edge cases and give the complexity before you write code, and treat those steps as part of the answer.
What to demonstrate
- Whether your code actually runs and passes tests, including ones you add, rather than looking plausible
- Whether you clarify the input and discuss edge cases (empty input, nulls, malformed data, extreme values) before coding
- Whether you state time and space complexity and choose data structures for a reason
- Whether you keep talking while you debug, so the interviewer can follow your reasoning
How to prepare
- Practise in a plain shared editor without autocomplete: write the function, then a small main that runs three or four cases and prints expected against actual
- Drill the reported shapes: a lazy nested-list iterator with a stack, a tree built from parent-child pairs with cycle and orphan checks, and a query parser that tokenizes before it maps tokens to filters
- For routing, know BFS for unweighted edges and Dijkstra for weighted ones, and say which the prompt needs before you write either
- Get a simple correct version running first, then optimise; avoid over-engineering early
Virtual Onsite Interview
reportedThe final stage is described as a virtual onsite with several technical and behavioral panels. Plan to be ready for both live coding and system design by this point, and hold your onsite coding to the same runnable-code standard as the phone screens. The reported design questions (see the questions section) come with no stated round, so prepare all of them before the onsite rather than assuming which one you will get. In design discussions, focus on practical trade-offs: storage choice, caching, queues and API protocols, measured against latency, consistency and network partitions. For the behavioral panels, prepare stories about project ownership, collaboration and empathy for both customers and pros.
What to demonstrate
- In coding panels, the same standard as the phone screens: code that runs, with edge cases and tests you raise yourself
- In design panels, whether you define API contracts and a data model, then justify storage, caching and queueing choices with concrete trade-offs
- Whether you name failure modes, such as a lost notification, a stale availability read or a hot geographic area, and describe how the design recovers
- In behavioral panels, whether your stories show ownership from design through launch and monitoring, and decisions made with the end user in mind
How to prepare
- Work through each reported system design prompt end to end: requirements, API, schema, the main read and write paths, then scaling and failure handling
- For practical design prompts, write working code: an interval store that rejects overlaps on add and edit, and a sliding-window limiter with a per-user timestamp queue
- Prepare one fact sheet per project story (scale, team, timeline, what broke) so the numbers stay the same if a later panel asks about the same project
- Run at least one mock that combines a coding problem you must execute with a design discussion
2 candidate reports. Individual accounts describe a particular role and hiring cycle.
Thumbtack Data Scientist Interview Experience — Ran Out of Time on the SQL Challenge, Still Got the Offer
View report detailsThumbtack Data Scientist Interview Experience — Stuck on the Final Modeling Round
View report detailsPracHub editorial advice for the preparation topics above.
Writing code in the phone screen or onsite that you never run
Candidates describe live coding rounds where code has to compile and pass test cases, so prepare as if pseudocode or an untested function counts as unfinished. Run a small version early, add your own cases for empty input, a single element and the tricky boundary, and fix what breaks while you explain it. Practise in an editor with no autocomplete so syntax slips do not use up your time.
Treating the in-memory database assessment as a script with one global map and a full-copy rollback
Treat the submission as code someone will review: readable, modular, tested and object-oriented. Keep command parsing, storage and transactions in separate parts. Implement ROLLBACK with a per-block undo log that records each key's prior value, including absence, instead of snapshotting the whole store. Keep a value-to-count map so NUMEQUALTO does not scan every key. Ship tests for nested rollback and for commit and rollback with no open transaction.
Calendar and interval logic that fails on touching events or on edits
For the reported calendar prompt (no overlapping events for a pro), say up front whether intervals are half-open [start, end), so back-to-back jobs do not conflict. Remember that an edit has to exclude the event being edited from its own conflict check. Keep events sorted, or in a tree keyed by start time, so a conflict check only looks at neighbours. Mention time zones, and test adjacent, contained and identical intervals.
Saying 'guaranteed delivery' or 'real-time matching' without defining what the system does on failure
For the notification prompt, state the delivery semantics (at-least-once with a deduplication key is the usual honest answer). Add retries with backoff, a dead-letter path, and rate limits per user and per provider channel. For lead matching and pro search, say where availability data can be stale, how the geographic index is partitioned, and what happens in a dense area. Tie each storage, cache and queue choice to its effect on latency, consistency or partition behaviour.
A deadline or disagreement story that ends with 'I worked harder' or 'we agreed'
The reported behavioral prompts ask about a project you led from inception to launch, a strong technical disagreement, and a deadline you realised you would miss. For each, name when you noticed the problem, who you told and how early, what evidence settled the disagreement, and what scope or quality trade-off you chose. Fix the numbers for each project in advance so they match if the same project comes up in a design panel.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Given a list of service categories and their parent-child relationship…
Given a list of service categories and their parent-child relationships, write a function to construct and traverse the category tree.
Approach
- Restate the input: its shape, its size, and what is guaranteed about it.
- Name the brute-force solution and its complexity before improving on it.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Implement an iterator that can traverse a nested list of integers, fla…
Implement an iterator that can traverse a nested list of integers, flattening the structure on the fly.
Approach
- State the target complexity and say which constraint rules the naive version out.
- Choose the data structure from the access pattern, not from familiarity.
- Name the brute-force solution and its complexity before improving on it.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Solve a string parsing and manipulation problem to format user search …
Solve a string parsing and manipulation problem to format user search queries into structured filters.
Approach
- Name the brute-force solution and its complexity before improving on it.
- Restate the input: its shape, its size, and what is guaranteed about it.
- State the target complexity and say which constraint rules the naive version out.
Follow-up
- Which test case would catch an off-by-one here?
- What is the worst case, and how likely is it on real data?
Validate a booking state machine for dead ends
You are given a booking state machine as a list of states, each flagged terminal or not, and a list of directed (from_state, to_state) transitions; at most 40 states and 400 transitions. Report three things: states unreachable from 'accepted'; declared terminal states that still have an outgoing transition; and non-terminal states from which no terminal state is reachable. Then validate a batch of observed transition sequences, up to 10 million transitions in total, against the graph. State the complexity of each part separately.
Approach
- Build both the adjacency map and its transpose as hash maps of sets, in O(V + E). The transpose is what makes the third question cheap, so build it up front rather than deriving it per query.
- Unreachable states: one BFS or DFS from 'accepted' over the forward graph; anything unvisited is unreachable, O(V + E). In production an unreachable state is usually a rename that was applied in code but not in the data.
- Terminal violations, as the prompt defines them: a declared terminal state with out-degree > 0. One scan of out-degrees against the flags answers it, O(V + E). Report the converse — a state with out-degree 0 that nobody declared terminal — as its own separately named list rather than folding it into this count. They are different defects: a terminal with out-edges is a wrong graph, an unlabelled sink is a missing flag. Every unlabelled sink is also, by construction, a dead end under the third check, so those two lists overlap and their sizes must never be added together.
- Dead ends: run one multi-source BFS seeded with every terminal state at once over the transpose. Any non-terminal state not reached can never terminate, which in production is a booking that holds a provider and an authorisation indefinitely. O(V + E). Seeding with a single terminal state gives a wrong answer for states that can only reach a different terminal.
- Sequence validation: check each consecutive pair against the adjacency set at O(1) per pair, O(L) for L transitions across the batch. Separately reject any sequence that continues after a terminal state — that is the out-of-order-event bug, not a graph defect, and conflating the two hides it.
- Note the sizes deliberately. V and E are tiny, so none of the first three answers is about speed; the complexity argument matters only on the 10-million-transition validation pass, where an O(V) linear scan of the edge list per pair would be the accidental quadratic.
Worked solution 25 min
- Take the states accepted, en_route, arrived, in_progress, completed, cancelled, disputed, with completed and cancelled declared terminal.
- Take the transitions: accepted to en_route and to cancelled; en_route to arrived and to cancelled; arrived to in_progress and to cancelled; in_progress to completed and to cancelled; completed to disputed.
- BFS forward from accepted and note which states are visited.
- Scan out-degrees against the terminal flags, producing two separate lists: declared terminals that still have an out-edge, and out-degree-0 states carrying no terminal flag.
- Multi-source BFS from {completed, cancelled} over the transpose and list the non-terminal states it fails to reach.
- Add a disputed-to-completed edge and re-run every check, naming which lists move and which do not.
Follow-up
- A 'started' event arrives after 'completed' for the same booking — where in a running service is that rejected, and what does the caller see?
- How do you version the transition table so that a deploy mid-saga does not invalidate bookings already in flight under the old table?
- Which of these transitions move money, and what does marking them add to this validation?
Build a per-request dispatch funnel with window functions
From dispatch_offer(offer_id, request_id, provider_id, attempt_no, offered_at, responded_at, status) and request(request_id, market_id, created_at), write one query returning a row per request created in a window with: offers issued, whether it matched, the ordinal of the accepted offer within the request, seconds from request.created_at to that offer's responded_at, and the gap in seconds to the previous matched request in the same market. Requests that expired with no offer at all must appear with zero offers. Then say what breaks if you bound the window on offered_at instead.
Approach
- Drive from
requestwith a LEFT JOIN todispatch_offer, never from the offer table. Starting from offers removes every request that found no supply, which is precisely the population the funnel exists to measure, and it makes the remaining numbers look better as supply gets worse. - Collapse offers per request with FILTER aggregates:
COUNT(d.offer_id)rather thanCOUNT(*)so the no-offer row counts as 0,BOOL_OR(d.status = 'accepted')for matched, andMAX(d.attempt_no) FILTER (WHERE d.status = 'accepted')for the winning ordinal. MAX is a projection rather than a reduction only if a request can have at most one accepted offer, so be exact about where that comes from. It is not the partial unique index used elsewhere in this library: that one is ondispatch_offer (provider_id) WHERE status = 'offered', which bounds concurrent live offers per provider and constrains nothing about rows already inaccepted, and nothing about requests at all. The guarantee isbooking (request_id)UNIQUE together with the guarded accept — the accept's status flip and the booking insert commit in one transaction, so a second offer on the same request loses on SQLSTATE 23505 and its status change rolls back with it. Where that pairing is absent, MAX silently returns one winner out of several rather than failing, so assert the premise withCOUNT(*) FILTER (WHERE d.status = 'accepted') <= 1instead of trusting it. - Apply the window after the grouping. PostgreSQL evaluates window functions after GROUP BY, so
LAG(MAX(d.responded_at) FILTER (WHERE d.status = 'accepted')) OVER (PARTITION BY r.market_id ORDER BY ...)is legal in the same SELECT list; what is not legal is referencing the alias you gave that aggregate. A CTE that groups, wrapped by a SELECT that windows, is the readable form and the one that survives someone adding a fourth metric. - Handle NULL deliberately rather than by reflex.
responded_atis NULL on expired offers, andseconds_to_matchmust stay NULL for an unmatched request; aCOALESCE(..., 0)there converts 'never matched' into 'matched instantly' and moves every average computed downstream. - Answer the boundary question: bounding on
offered_atsplits a request offered at 23:59:52 and accepted at 00:00:07 across two windows and drops requests that received no offer at all. Bound onrequest.created_at, accept that the tail of the window is still in flight, and either exclude a trailing grace period or mark those rows as censored so a re-run does not quietly change yesterday's number.
Follow-up
- Add the provider side: for each provider, the longest run of consecutive declines ending inside the window. Which window functions, and why is that a gaps-and-islands problem?
- This table holds 2 billion rows. Which index serves the window bound, and would you pre-aggregate into a per-minute rollup instead?
- Your p50 time-to-match improves week over week while the match rate falls. Explain how the query you just wrote produces exactly that pattern.
Resolve accept-versus-expire races with a guarded conditional update
Two dispatch partitions each read provider_presence.status = 'idle' for provider 88 from an asynchronous replica and each try to offer them a request. At the same moment the TTL sweeper is expiring offer 4471 while that provider taps accept. The database is PostgreSQL at the default READ COMMITTED. Write the exact statement that accepts an offer, the statement the sweeper runs, and the index DDL that makes a second concurrent live offer to provider 88 impossible. For every affected-row count your accept can return, say what the caller does.
Approach
- Make the accept a single guarded write rather than a read followed by a write:
UPDATE dispatch_offer SET status = 'accepted', responded_at = now() WHERE offer_id = $1 AND status = 'offered' AND expires_at > now() RETURNING request_id, provider_id, quote_id;. The affected-row count is the authoritative answer to 'did I win', and nothing else in the system is. - Explain why that is safe at READ COMMITTED specifically. When the sweeper holds the row lock, the accept's UPDATE blocks; on the sweeper's commit PostgreSQL re-evaluates the WHERE clause against the new row version, finds
status = 'expired', and matches zero rows. A priorSELECTwould have read a snapshot taken before the sweeper committed and decided on stale data, with nothing re-checking it. - Map both outcomes to caller behaviour. One row means you won: insert the booking and the outbox row in the same transaction, and keep the payment gateway call outside it. Zero rows means you lost, and the caller must re-read to tell the provider why — expired, rescinded, or already declined — and must not retry the UPDATE, because a retry cannot change a terminal status.
- Give the sweeper the mirror-image guard so the two cannot both win:
UPDATE dispatch_offer SET status = 'expired' WHERE status = 'offered' AND expires_at <= now();. - Push the cross-partition duplicate down to the storage layer, since partition-local reasoning cannot see it:
CREATE UNIQUE INDEX CONCURRENTLY provider_one_live_offer ON dispatch_offer (provider_id) WHERE status = 'offered';. The losing partition's INSERT raises SQLSTATE 23505, which the dispatcher treats as 'candidate taken, take the next one' rather than as an error. Pair it with the same shape onbooking (provider_id) WHERE status IN ('accepted','en_route','arrived','in_progress'). - Compare with the isolation-level answer: SERIALIZABLE prevents the same anomaly but reports it as a 40001 serialization failure that every caller must be written to retry, and the retry arrives at the same decision — so you buy retry machinery to learn what the affected-row count already told you for free.
Worked solution 30 min
- Open two sessions, run the guarded UPDATE for the same
offer_idin both without committing, and watch the second block. - Commit the first and observe the second return
UPDATE 0without error. - Repeat the experiment with a naive SELECT-then-UPDATE and observe both sessions commit an accept.
- Create the partial unique index and attempt two INSERTs of an
offeredrow for the same provider from separate sessions.
Follow-up
- The offer transition, the booking insert and the outbox row must commit together. Sketch that transaction, and say what goes wrong if the payment authorisation is inside it.
- A partial unique index's predicate must be immutable. Why can it not be
WHERE expires_at > now(), and what do you use instead? - The provider's phone times out and retries the accept, but the first call actually succeeded. What does the second call return under your design, and is that acceptable?
Design a real-time lead generation and matching system that connects c…
Design a real-time lead generation and matching system that connects customers requesting a service with available local pros in real-time.
Approach
- State the consistency you need, and where you are willing to be stale.
- 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.
Follow-up
- What breaks first when traffic grows ten times?
- What would you drop to keep the system up under load?
Design a high-throughput search and discovery system for local service…
Design a high-throughput search and discovery system for local service pros, taking into account geographical proximity, availability, and user ratings.
Approach
- Fix the scope first: who calls this, how often, and what they do when it fails.
- Choose a partition key and say what query it makes expensive.
- Name the failure you are designing for, then the recovery path.
Follow-up
- What breaks first when traffic grows ten times?
- How does this behave when that dependency is down for an hour?
Design a scalable notification service capable of sending millions of …
Design a scalable notification service capable of sending millions of push notifications, SMS, and emails daily, with guaranteed delivery and rate-limiting.
Approach
- Name the read and write paths separately; they rarely have the same bottleneck.
- Name the failure you are designing for, then the recovery path.
- Choose a partition key and say what query it makes expensive.
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?
Design and implement an in-memory rate limiter that restricts the numb…
Design and implement an in-memory rate limiter that restricts the number of API requests a user can make within a rolling time window.
Approach
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
- Say what you would check first and why it is the highest-information step.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Redesign an offer-accept endpoint whose retry cannot read its own result
dispatch_offer holds one row per (request, provider, attempt) with status in offered, accepted, declined, expired, rescinded, an expires_at, a responded_at, and a partial unique index on provider_id WHERE status = 'offered'. The current API is PATCH /offers/{offer_id} with a status field, answering 200 or 409. A provider accepts at second 14 of a 15 s window, the response is lost, and the app retries. The retry finds status = 'accepted' and returns 409, so a driver who is already assigned is told the job went elsewhere. Redesign the request and response.
Approach
- Diagnose it as a modelling error, not a retry bug. PATCH with a target status invites last-write-wins on a column, and the response collapses two different worlds - 'you already won this' and 'this is no longer yours' - into one 409. Replace it with an action scoped to the resource, POST /offers/{offer_id}/acceptance with an empty body: offer_id already identifies exactly one provider's chance at one request, so the URL is the idempotency scope and no client-supplied key is needed.
- Make the write decide the race: UPDATE dispatch_offer SET status = 'accepted', responded_at = now() WHERE offer_id = :id AND status = 'offered' AND expires_at > now() RETURNING request_id, quote_id. One affected row means you won, zero means you did not, and no read before the write is trustworthy because presence and offer state are read from an asynchronously replicated snapshot. Under READ COMMITTED a concurrent writer makes this statement block and then re-evaluate its predicate against the committed row version, which is exactly the behaviour the guard relies on; a SELECT-then-UPDATE pair does not get that.
- Classify the zero-row case from the row itself, which is what the old shape could not do. status = 'accepted' on this row means this provider accepted, so return 200 with the booking - this is the lost-response case. status = 'rescinded' returns 409 with a code saying the request went elsewhere; 'expired' or a passed expires_at returns 410; 'declined' returns 409 with its own code. Each maps to a different screen in the app.
- Return the booking representation from the accept itself, not a bare status. Inside a 15 s window a follow-up GET is another round trip on a cellular link, and it reintroduces the ambiguity you just removed. Insert the booking in the same transaction as the offer transition and write the outbox row there too.
- Enforce the second rule declaratively: a partial unique index on booking(provider_id) WHERE status IN ('accepted','en_route','arrived','in_progress') stops a provider holding two live bookings, and the constraint violation maps to 409 with a specific code rather than a 500. Note the precondition - a partial index predicate must be immutable, so expires_at > now() cannot live there; offer expiry is enforced in the UPDATE predicate and by the sweeper.
Worked solution 30 min
- Replace the PATCH route with the acceptance sub-resource and state that offer_id is the idempotency scope.
- Write the guarded UPDATE with RETURNING and make the affected-row count the only decision input.
- Write the zero-row classifier as one SELECT of status, provider_id and responded_at, mapping each status to a status code plus machine code.
- Create the booking and the outbox row in the same transaction, and map the booking partial-unique violation to a 409 code rather than letting it surface as a 500.
- Reproduce the reported defect: accept, discard the response, retry, and assert the retry returns 200 with the same booking_id.
- Race the sweeper's UPDATE ... WHERE status = 'offered' AND expires_at < now() against the accept and assert exactly one reports one affected row.
Follow-up
- The TTL sweeper and the accept run at the same instant. Walk through both statements and say which one reports an affected row.
- The accept commits but the process dies before the relay publishes the outbox row. What does the provider see, and what repairs it?
- How would you extend this to a batched offer sent to three providers at once, where exactly one must win?
Two active bookings per provider only in adjacent dense markets
About twelve providers a day end up holding two non-terminal bookings, out of two million offers. It happens only at peak, only in two adjacent dense markets, never in staging, and the rate fell almost to zero for a week when verbose per-offer logging was enabled. booking is partitioned by market_id and carries a unique index the team added on (market_id, provider_id) restricted to non-terminal statuses; dispatch_offer has a partial unique index on provider_id where status = 'offered'. Give the ordered checklist and the fix.
Approach
- Characterise the duplicate pairs before theorising about isolation levels. For each provider holding two live bookings, tabulate the two rows' market_id, the delta between their accepted_at values, and which dispatch partition issued each offer. If the market_ids differ, the constraint that exists could never have applied and the entire isolation-level discussion is premature.
- Explain why the index does not cover the case. A unique constraint on a partitioned table must include every partition key column, so (market_id, provider_id) is the only unique index the team could have created -- attempting it on provider_id alone is rejected outright. It therefore enforces uniqueness within a partition. A provider visible to two adjacent markets takes one booking in each, and each insert satisfies its own partition's index with no error raised anywhere.
- Address the part that misleads everyone, which is the logging correlation. The surviving window sits between the guarded accept on dispatch_offer and the booking insert. Verbose logging added I/O inside that window and shifted the interleaving, narrowing the overlap without closing it; staging runs a single dispatch partition over the provider pool and so never generates the interleaving at all. A rate that moves when you add instrumentation is evidence of a timing window, not evidence that the instrumentation fixed anything.
- Confirm the remaining race rather than assuming it. Under READ COMMITTED, two transactions in two partitions each read 'no non-terminal booking for this provider' and each insert, because check-then-act offers no guarantee the read is still true at write time. Asynchronous presence replication makes 'idle' a stale read independently of this, so neither read can be trusted as a decision.
- Fix with one global serialization point that is not partitioned: an unpartitioned claims table with provider_id as the primary key holding the current active booking, inserted in the same transaction as the booking row. The unique violation on that insert is the authoritative signal that this caller lost -- handled by rescinding the offer and returning the request to dispatch, never as a 500. The row is deleted on the transition to a terminal state, which is the only place the invariant may be released.
- Verify that the constraints you believe are in force actually are. An index built concurrently that failed is left marked invalid and cannot be relied on to enforce uniqueness; check the validity flag on every partition's index before concluding a constraint was protecting anything. Then add the standing audit query, because this class of bug raises no errors and is visible only in the data.
Follow-up
- The claims table is now a globally hot row per provider. What is the contention shape at peak, and what would you measure before treating it as a problem?
- Compare this with taking the booking insert at SERIALIZABLE: which failures does each approach catch, and what does each cost the caller?
- A booking ends by process crash rather than by a transition, so the claim row is never deleted. How do you detect and release it without racing a live assignment?
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.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Recruiter questions and a runnable-code setup
- Prepare the recruiter call: a short background summary, your career goals, and questions about whether the assessment is an online test or a take-home, which platform and language, and whether there are one or two phone screens
- Set up plain-editor practice in your interview language with no autocomplete, plus a tiny test harness pattern: a main that runs cases and prints expected against actual
- Solve two easy problems end to end in that setup, such as an array scan that returns the indices of values below a threshold and a token frequency count, running every case you write
Deliverable: A written list of recruiter questions and two solved problems that run with their own test cases.
Practice prompt ↗Practice prompt ↗Practice prompt ↗Worked solution ↗02Technical assessment: the in-memory database
- Implement GET, SET, UNSET and NUMEQUALTO with separate command parsing and storage
- Add BEGIN, ROLLBACK and COMMIT using a stack of undo logs that record prior values, including 'was absent', and keep NUMEQUALTO counts correct through rollback
- Write tests for nested rollback, commit with nested blocks, rollback and commit with no open transaction, and NUMEQUALTO after unset
- Add a short README with the assumptions and the complexity of each command, as you would for a take-home
Deliverable: A working, tested in-memory database with transactions and a README of assumptions and complexity.
Practice prompt ↗Practice prompt ↗03Coding: iterators, trees and parsing
- Implement the reported nested-list iterator lazily with a stack, and test empty sublists, deep nesting and hasNext called twice
- Build a category tree from parent-child pairs, traverse it, and handle a missing parent and a cycle explicitly
- Write a search-query parser that tokenizes first and then maps tokens to structured filters, and test extra spaces, unknown tokens and empty input
- Evaluate a postfix expression with a stack and state its complexity
Deliverable: Three runnable solutions with edge-case tests and a stated complexity for each.
Practice prompt ↗Practice prompt ↗04Coding: graphs, ordering and state
- Solve the reported routing prompt for a pro travelling between job sites: say whether edges are weighted, then implement BFS or Dijkstra accordingly
- Practise a dependency-order check with topological sort and cycle detection
- Work through the worked exercise 'Validate a booking state machine for dead ends' and check your reachability lists against its expected result
- Compute a streaming mean and median over a bounded integer range with frequency counts
Deliverable: Graph solutions that run, plus the state-machine exercise checked against its stated result.
Practice prompt ↗Practice prompt ↗Worked solution ↗05Object-oriented and practical design, in code
- Implement the reported calendar for one pro: add, edit and delete with no overlaps, half-open intervals, and an edit that excludes itself from the conflict check
- Implement a rolling-window rate limiter per user with a timestamp queue, then describe the memory trade-off against a counter-based approximation
- Revisit the key-value store with transactions from day 2 and refactor anything you would not want a reviewer to read
Deliverable: Tested calendar and rate-limiter classes with a short note on the design choices.
Practice prompt ↗Practice prompt ↗06System design
- Design the reported notification service: channels (push, SMS, email), delivery semantics, retries, dead letters and per-user rate limits
- Design pro search by proximity, availability and rating: a geographic index, where availability can be stale, and caching of results
- Design real-time lead matching, then work through the worked exercises 'Redesign an offer-accept endpoint whose retry cannot read its own result' and 'Resolve accept-versus-expire races with a guarded conditional update'
- For each design, write the API, the schema, and one justified choice each for storage, cache and queue
Deliverable: Three designs, each with API, schema and named failure modes, plus both concurrency exercises compared against their expected results.
Practice prompt ↗Practice prompt ↗07Behavioral stories and a full mock
- Write stories for the reported prompts: a project you led from inception to launch, a strong technical disagreement, and a deadline you saw you would miss
- For each story, fix the figures (scale, team, timeline) and name the trade-off, the evidence and what you would change
- Run a mock with a coding problem you must execute in a shared editor, followed by one design prompt from day 6
- Review the mock: note any code that did not run on the first try and any design choice you could not justify
Deliverable: Three behavioral stories with fixed figures and written notes from one combined coding and design mock.
Practice prompt ↗Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
Prepare stories around three themes: owning a feature from design through launch and monitoring, collaborating (mentoring, giving and taking feedback, resolving disagreements), and empathy for both sides of the marketplace, customers and pros. For each story, give the decision you made, the evidence behind it and the result, and keep the figures the same if the project comes up again in a design panel.
Design a calendar scheduling system with functionalities to add, edit,…
Design a calendar scheduling system with functionalities to add, edit, delete, and update events, ensuring no overlapping conflicts for a service pro.
Approach
- Close with what you would do differently, concretely.
- 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?
Tell me about a time when you had a strong technical disagreement with…
Tell me about a time when you had a strong technical disagreement with a teammate or manager. How did you resolve it?
Approach
- 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.
- 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?
Estimating work in a system you had never touched
You are asked for a date on work you have never done: moving live provider positions out of the transactional database into a spatial index with TTL semantics, or rebuilding payouts on append-only double-entry postings. You have two days before the estimate is due. Describe how you built it - what you decomposed, what you spiked, what you refused to estimate at all - and the uncertainty you attached. State the estimate you gave and its shape: a range with a confidence, or a first milestone with a named checkpoint. Then say what it turned out to be, and what you would change about the method.
Approach
- Split the work into done-before, analogous, and genuinely unknown. Only the third bucket needs a spike, and estimating that bucket without one is a guess wearing a number.
- Spend the two days buying information about the largest unknown rather than the largest task. For a store migration the unknowns are almost never the new code - they are the backfill, the set of consumers reading the old shape, and how you will prove the new path is equivalent under live traffic.
- Give the estimate a shape that survives being wrong: a range with an explicit confidence, or a first milestone with a date plus a checkpoint at which everything after it is re-estimated. A single number covering six weeks of novel work is a promise whose failure is already scheduled.
- Attach assumptions as a numbered list. A written assumption converts a later slip from a broken commitment into a discovered fact, which is the only mechanism that keeps estimates honest over a long project.
- Compare estimate to actual and attribute the variance to one cause - a consumer nobody knew about, review latency, an unowned dependency, or scope growth. The correction differs for each, and naming the wrong one means repeating the miss.
Follow-up
- Which assumption broke first, and how long before you noticed?
- Knowing what you know now, what would you have spiked instead?
- How do you answer when the person asking wants a single date and will not accept a range?
- 01
Describe a challenging technical project you led from inception to launch. What trade-offs did you make, and what did you learn?
- 02
Tell me about a time you had a strong technical disagreement with a teammate or manager. How did you resolve it?
- 03
How do you handle a situation where you realise a project you are working on will not meet its deadline?
- 04
Tell me about a delivery you led that had measurable business impact: how you prioritised and kept stakeholders aligned.
- 05
Describe a technical decision where thinking about the end user changed what you built.
Is this an official Thumbtack interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Thumbtack. Rounds and questions reflect what candidates have reported, not a process Thumbtack has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗What rounds does the Thumbtack Software Engineer process include?
Candidates report four stages: a recruiter call about background, goals and team alignment; a technical assessment, either an online assessment or a take-home challenge; one or two technical phone screens with senior engineers; and a virtual onsite with several technical and behavioral panels. Candidate reports put the whole process at roughly three to five weeks. Ask your recruiter which assessment format and how many phone screens apply to you.
PracHub Software Engineer practice ↗Does my code need to compile and run during the live interviews?
Candidates describe writing code in a shared editor that must compile and pass test cases, in the phone screens and in the onsite coding rounds. Prepare for that: practise in your chosen language without autocomplete, know its standard library well, and get used to writing and running your own test cases while you talk through them.
PracHub interview research ↗What is the in-memory database assessment?
Candidates report it as a common form of the technical assessment, given as a timed online test or a take-home. You build a command-driven key-value store supporting commands such as GET, SET, UNSET and NUMEQUALTO, with nested transactions through BEGIN, COMMIT and ROLLBACK. Treat the submission as code someone will review: keep it readable, modular, tested and object-oriented, alongside producing correct output.
PracHub Software Engineer practice ↗What kinds of coding and design questions are reported?
Coding reports include a nested-list iterator, building and traversing a category tree, parsing search queries into filters, and graph routing between job sites. Practical design reports include a calendar with no overlapping events, a key-value store with transactions, and a rolling-window rate limiter. System design reports include real-time lead matching, a high-volume notification service with guaranteed delivery and rate limiting, and pro search by proximity, availability and ratings. The reports do not tie these to specific rounds.
PracHub Software Engineer practice ↗How should I prepare for the behavioral panels?
Prepare stories for the reported prompts: a project you led from inception to launch, a strong technical disagreement, and a deadline you realised you would miss. Build them around ownership, collaboration and empathy for customers and pros. In each story, name the decision, the evidence and the outcome, and keep your project figures the same across panels.
PracHub Software Engineer practice ↗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