Interview guides · Aspen Dental
Aspen Dental Software Engineer Interview Guide
Aspen Dental Software Engineer interview guide: reported questions on SQL tuning, queues, recursion and memory leaks, plus a 7-day prep plan.
The 60-second brief
Aspen Dental runs a large network of dental offices, and candidates describe the software engineer role as building and maintaining the systems those offices depend on, including patient records and internal platforms. The reported work spans database optimization, application code that goes through peer review, and features owned from the first requirement to deployment, in partnership with product managers and other engineering teams. For preparation, that points to relational data, reliable code and the ability to turn a business requirement into a technical plan.
This guide is for software engineer candidates preparing for Aspen Dental. It covers the five questions candidates report (slow-query tuning, sorting complexity, choosing a queue structure, recursion edge cases and debugging a production memory leak), the algorithm and data-structure topics candidates list, the SQL and schema-design material that goes with a database-heavy role, behavioral preparation, and a seven-day plan. It does not cover other roles.
Candidates describe a path that starts with an initial screening and moves to technical and managerial assessments, with remote steps and possibly an onsite or virtual final sequence. No round-by-round order or length is reported, so confirm the format with your recruiter.
Focus areas
- Algorithms
- Data Structures
- Algorithmic Reasoning
Overview
Reports for this role point to a fundamentals-first loop. The technical topics candidates list are arrays and strings, linked lists and trees, and hash maps, with example scenarios such as finding a pair of integers that sums to a target and building a stack using only queues. The reported questions add four themes: tuning a slow query on a large database, explaining the time complexity of a sorting solution, choosing a data structure for a queue, and debugging a memory leak in production. Preparing means being able to write the code and also say out loud why the structure and the complexity are what they are.
The requirements candidates describe make SQL and relational design a first-class topic, not an afterthought: a strong grasp of relational databases and SQL is listed next to proficiency in an object-oriented language (Java, C# or Python are the examples), Git, and the ability to solve algorithmic problems efficiently. Reports also place database schema design in the later, system-design-flavored stages. Be ready to talk about keys, constraints, indexes and how a table layout affects the queries that run against it, since the same knowledge underpins the slow-query question.
Cloud experience (AWS or Azure), a front-end framework (React or Angular) and Agile or Scrum experience are described as nice to have rather than required, so give them light preparation: know what you have done with them and be honest about what you have not. Candidates are advised to think aloud while coding, to use the STAR structure for behavioral and situational questions, and to be able to discuss any project on the resume in depth, including the technical hurdle. Some candidates also report uneven communication from the hiring side, so keep your own list of who to follow up with and when.
Where to focus your preparation
Candidates do not report a fixed list of named rounds for this role, so the guide treats the loop as one preparation surface and leaves the sequence for you to confirm with your recruiter.
Preparation focus
Candidates describe a progression from an initial screening, often remote, to technical and managerial assessments, and some mention an onsite or virtual final sequence. How many stages there are, in what order and how long each lasts is not reported, so ask your recruiter which stages involve live coding, which involve a design or schema conversation and which are conversational. The reported content is algorithms and data structures (arrays, strings, linked lists, trees, hash maps), SQL and database tuning, production debugging, schema design in later stages, and behavioral or situational questions. Prepare each as something you can do on a shared editor or whiteboard while narrating your reasoning, which candidates are advised to do.
What to demonstrate
- Working code and a stated time and space complexity on core structures: arrays, strings, linked lists, trees and hash maps
- Relational database and SQL fundamentals, including how you would find and fix a slow query
- How clearly you explain trade-offs and decisions while you work, which candidates are advised to do aloud
- Behavioral and situational answers about communication, conflict and projects on your resume
How to prepare
- Write the pair-sum-to-target solution with a hash map and the stack-built-from-queues exercise in your chosen language (Java, C# or Python), and say the complexity of each as you finish
- Practise the five reported questions aloud: query tuning, sorting complexity, choosing a queue structure, recursion edge cases and a production memory leak
- Sketch a small relational schema (for example patients, appointments and practices) with keys and indexes, then write two queries against it and explain how you would speed one of them up
- Prepare three STAR stories from your own projects, each with the technical hurdle you hit and what you decided
Where candidates lose points
Giving the complexity of a sorting solution without naming the algorithm or the input assumptions
State the algorithm and its cost together: comparison sorts such as merge sort are O(n log n), quicksort is O(n log n) on average but O(n^2) in the worst case, and counting sort is O(n + k) only when the key range is small. Say what extra memory it needs and whether it is stable.
Choosing a structure for a queue by familiarity and ignoring the cost of removing from the front
Check each operation. A plain array makes dequeue O(n) when you shift elements, a linked list gives O(1) at both ends with extra pointers, and a circular buffer or a deque gives O(1) in contiguous memory. Say which you pick and why the access pattern decides it.
Tuning a slow query by guessing at an index
Start from the execution plan. Find the step that scans many rows or sorts on disk, check whether the filter and join columns are indexed and whether a function on a column makes the index unusable, then change one thing and compare the plan again.
Answering the production memory leak question with restarting the service or adding memory
Describe a sequence: confirm that memory grows steadily rather than spiking, find what changed, take heap snapshots or profiles at two points in time and compare which objects grow, trace what holds the references (caches without eviction, listeners never removed, unclosed resources), then ship a fix and watch the graph.
Writing recursive code with no stated base case, depth limit or empty-input handling, or coding silently
Say the base case before you write the recursive step, trace one tiny input, and narrate decisions as you code, since candidates are advised to think aloud. A correct solution you cannot explain is weaker than an explained one with one fixable bug.
Questions & practice
Can you explain the time complexity of your solution for this sorting problem?
Medium · Technical Foundations and Algorithms
Approach
- Give the complexity in terms of n, the input size, and break it into steps. If the solution sorts and then makes one linear pass, sorting dominates: O(n log n) time. Add the space the sort itself uses, not just your own variables.
- Know the main algorithms by their deciding properties. Merge sort is O(n log n) in every case, stable, with O(n) extra space. Quicksort is O(n log n) on average, O(n^2) worst case with bad pivots (for example, first-element pivot on sorted input), with O(log n) expected stack. Heapsort is O(n log n) worst case with O(1) extra space but is not stable. Insertion sort is O(n^2) worst case but O(n) on nearly sorted input.
- Know what your language's built-in sort does. Python's sorted and list.sort use Timsort: stable, O(n log n) worst case, close to O(n) on already-ordered runs. Java's Arrays.sort uses dual-pivot quicksort for primitives (not stable) and TimSort for objects (stable). .NET's Array.Sort and List<T>.Sort use introsort, which is not stable, while LINQ OrderBy is stable.
- Mention the lower bound and how to get around it. Comparison sorts need Omega(n log n) comparisons in the worst case. Bounded integer keys allow counting sort in O(n + k) or radix sort in O(d(n + b)).
- Say whether sorting was the right choice at all. For a pair-sum style problem, sort plus two pointers is O(n log n) time with little extra memory. A hash set is O(n) expected time but uses O(n) memory. Name which one you would pick and why.
Follow-up
- You only need the k largest values. What do a size-k heap (O(n log k)) and quickselect (O(n) average) cost compared with a full sort?
- The data does not fit in memory. How does an external merge sort work, and what dominates its cost?
- Does stability matter for your solution, for example when sorting by a second key after a first?
Which data structure would you choose to implement a queue, and why?
Medium · Technical Foundations and Algorithms
Approach
- Start from the operations. A queue needs enqueue at the tail and dequeue at the head, both in O(1). Then compare the structures that give that.
- A singly linked list with head and tail pointers gives O(1) for both operations, but pays a separate allocation per node and has poor cache locality. A circular array (ring buffer) keeps head and count indices and wraps with modulo. It is cache-friendly and gets amortized O(1) enqueue if it doubles when full, copying the elements out in queue order.
- Call out the naive version: a dynamic array that removes from index 0 is O(n) per dequeue. In Python, list.pop(0) is O(n), so use collections.deque. In Java, prefer ArrayDeque over LinkedList. In C#, Queue<T> is already a circular array.
- Know the two-stack queue too. Push onto an inbox stack. On dequeue, if the outbox is empty, move everything from inbox to outbox. Each element moves at most once, so dequeue is amortized O(1) while a single call can be O(n).
- Cover the edge cases. Decide whether dequeue or peek on an empty queue throws or returns a sentinel. In a ring buffer, full and empty both have head equal to tail unless you track a count or leave one slot unused. Under concurrent producers and consumers you need a lock with condition variables, or a library type such as Java's ArrayBlockingQueue or Python's queue.Queue.
Follow-up
- Reverse it: implement a stack using only queues. Which operation becomes O(n), and why?
- How would you make the queue bounded and thread-safe, and what should a producer do when it is full?
- When would you need a priority queue instead, and what does a binary heap cost per operation?
Archive a resource graph without breaking live references or recursing
Medium · graph traversal · topological order · tenant isolation
Resources reference other resources within a tenant; for the largest tenant the reference table holds up to 2,000,000 nodes and 8,000,000 edges. Archiving a resource must archive everything reachable from it that nothing outside the set still references, refuse when a live external referrer exists, and terminate when references form cycles, which they legitimately do. Produce the archive order and the refusal list, targeting O(V+E). Say what stops the traversal crossing a tenant boundary, and why recursion is the wrong control structure at this size.
Approach
- Load the subgraph with the tenant predicate on both endpoints of the edge, not only on the side you started from. Scoping the left table alone is the classic cross-tenant leak: one mis-entered edge then pulls another tenant's resources into the traversal and, worse, into the archive.
- Traverse iteratively with an explicit stack. A 2,000,000-node graph can hold a chain deep enough to exhaust a native stack in the low tens of thousands of frames, and that failure is a process crash rather than an error you can return.
- Treat cycles as data rather than corruption: compute strongly connected components with Tarjan in O(V+E) using its own explicit stack, then condense. The condensation is a DAG, so a topological order over it gives the archive order, and every member of a component archives in one transaction because no order within a cycle is valid.
- Decide refusals with reverse edges. A candidate is archivable only if every in-edge originates inside the candidate set, so build the transpose or count in-degrees restricted to the visited set, and emit each blocked resource with the id of the external referrer, which is the only part of the answer an operator can act on.
Follow-up
- The graph is read in one query and the archive writes a minute later. What can change in between, and how do you make the write safe?
- The candidate set is 400,000 resources. Is that one transaction, and if not, what does a half-finished archive look like to a reader?
How would you approach optimizing a slow-running query in a large database?
Medium · Technical Foundations and Algorithms
Approach
- Measure before changing anything. Get the actual plan with EXPLAIN (ANALYZE, BUFFERS) in PostgreSQL or the actual execution plan in SQL Server. Find the node where the time goes: a sequential scan over a large table, a nested loop running once per outer row, or a sort or hash spilling to disk. Then compare estimated rows with actual rows.
- Fix access paths with the right index. In a composite index, put the equality-filter columns first, then the range or ORDER BY column. Use a covering index (INCLUDE) when an index-only scan would remove the table lookups. Some predicates cannot use a plain B-tree index: a function or implicit cast on the column (WHERE LOWER(email) = ...) and a leading-wildcard LIKE '%term'. Those need an expression index or a rewritten predicate.
- If estimates are far from actuals, suspect the optimizer's inputs before the query text. Look for stale statistics (run ANALYZE or UPDATE STATISTICS), skewed data, or SQL Server parameter sniffing caching a plan built for an unrepresentative value.
- Rewrite the query shape where the plan points. Select only the columns you need. Filter before joining or aggregating. Turn a correlated subquery into a join or EXISTS. Replace a large OFFSET with keyset pagination (WHERE id > last_seen ORDER BY id LIMIT n).
- State the trade-off and how you verify. Every index slows INSERT and UPDATE and costs storage, so re-run the plan and check the write path. If it is still slow, the next options are partitioning by the filter column, a summary or materialized table refreshed on a schedule, or moving reporting reads to a replica.
Follow-up
- How would you add that index to a large table that takes writes all day without blocking them?
- The query is fast in staging and slow in production with the same SQL. What differs, and how do you check each difference?
- Why might the optimizer ignore an index you just created, and when is that the right decision?
Hold a per-tenant active cap against concurrent creates
Hard · write skew · isolation levels · constraints
A tenant on the standard plan may hold at most 50 resources with status='active'. The create handler runs SELECT count(*) FROM resource WHERE tenant_id = $1 AND status = 'active', compares to 50, then inserts. Two creates arrive 3 ms apart on different instances and the tenant lands at 51. Name the anomaly, say whether PostgreSQL 16 READ COMMITTED or REPEATABLE READ prevents it and why, then give an implementation that holds the cap at READ COMMITTED with the exact statements. Finally, say what changes when the cap is 'at most one running export per tenant' on job_run.
Approach
- Name it: write skew. The two transactions read an overlapping set and write disjoint rows, so there is no row-level conflict for the engine to detect and each commit is individually legal.
- Rule out the levels precisely. READ COMMITTED takes a fresh snapshot per statement and takes no lock on the counted rows, so both see 49. PostgreSQL's REPEATABLE READ is snapshot isolation: it removes non-repeatable reads and phantoms within the snapshot but still admits write skew, because the anomaly is not a re-read of a changed row, it is a read of a set that a concurrent transaction invalidates. Only SERIALIZABLE closes it, by tracking the read dependency and aborting one transaction with SQLSTATE 40001 — a guarantee that exists only if the application re-runs the whole transaction from the read.
- Convert the set predicate into a single-row conflict: keep tenant.active_resource_count and run UPDATE tenant SET active_resource_count = active_resource_count + 1 WHERE tenant_id = $1 AND active_resource_count < 50 in the same transaction as the INSERT. Zero affected rows is the cap, returned as 409. The row lock serialises the decision at any isolation level, and contention is bounded to one tenant's row — which is also the fair-scheduling unit, unlike a global counter that would convoy every tenant behind one row.
- State the cost you just took on: a counter is a second source of truth that can drift, so every path that changes status must adjust it inside the same transaction, and a periodic reconciliation has to exist, with resource_revision as the authority for what the count should have been.
Follow-up
- A resource moves from archived back to active. Which statements change, and what breaks if the counter update and the status change land in different transactions?
- The cap becomes plan-dependent and a plan can change mid-month. Where does the number 50 live, and who reads it?
Make the resource write endpoint safe for retrying clients
Easy · idempotency · unique constraints · retries
The edge API accepts POST /v1/resources at 1.2k writes/second peak under a 400 ms p99 budget, and clients retry on timeout. idempotency_key has PRIMARY KEY (tenant_id, idempotency_key) plus request_fingerprint CHAR(64), state in ('in_flight','succeeded','failed'), response_status, response_body, resource_id, locked_until and expires_at. Specify the exact sequence the handler runs: what the second request does when it arrives while the first is still executing, what a retry carrying a different body receives, what happens when a process dies mid-request, and how the table is kept from growing without bound.
Approach
- Start from the race rather than the happy path. Insert the key row first, in its own short transaction, with state 'in_flight' and locked_until set to now() plus the request deadline. The unique constraint on (tenant_id, idempotency_key) is the arbitration mechanism: exactly one caller commits the insert, and every other caller takes a unique violation (SQLSTATE 23505) and becomes a follower. Reading the table and then inserting is a check-then-act race that both callers pass, and they pass it most often under the load that generates the retries.
- Give the follower a branch for every state it can observe, with no fall-through to doing the work: fingerprint mismatch means the key is being reused for a different request, so reject with 422 and never replay the stored response; 'succeeded' replays response_status and response_body verbatim; 'failed' allows a fresh attempt; 'in_flight' with locked_until in the future is answered with 409 and a retry hint, or a short bounded wait, because the only alternative is executing the effect twice.
- Commit the effect and the record of the effect together. The resource insert, its resource_revision row, its outbox_event row and the transition of the key row to 'succeeded' all happen in one transaction, so no crash can leave the work done and the key still 'in_flight'. The earlier insert is deliberately a separate transaction: it has to be visible to a concurrent caller before the work starts, which an uncommitted row is not.
- Close the wedged-key path. A process that dies after the insert leaves a row that would otherwise block that key forever, which is what locked_until exists for. Reclaim with a single conditional statement, UPDATE ... SET state='in_flight', locked_until=now()+interval WHERE ... AND state='in_flight' AND locked_until < now(), and treat a zero rowcount as losing the reclaim. Two statements that read and then update reintroduce the original race one layer down.
Follow-up
- The stored response body averages 200 KB and this table is now the largest in the database. What do you store instead, and what does a replay return once the body has been pruned?
- A client library generates a fresh idempotency key on every attempt. What breaks, which layer should have caught it, and does the server have any defence?
Make resource creation safe for a client that retried a timeout
Medium · idempotency · retries · race conditions
POST /v1/resources creates a resource row and, in the same transaction, a resource_revision and an outbox_event. A partner's HTTP client times out at 2 seconds and retries twice; under load the first request is still executing when the retry arrives, and duplicate resources are appearing. Using idempotency_key (tenant_id, idempotency_key, request_fingerprint, state, response_status, response_body, resource_id, locked_until, expires_at), specify the whole contract: first call, concurrent retry, retry after completion, a key reused with a different body, and what the client does with each response.
Approach
- Name why the plain REST shape fails here. POST is not idempotent, a timeout leaves the client unable to distinguish a lost request from a lost response, so it must retry - and the retry is a second create. Checking whether the key exists and then inserting it is check-then-act: under exactly the concurrency that caused the timeout, both attempts look, both see nothing, both proceed.
- Let the unique constraint arbitrate instead. INSERT the key row with state 'in_flight' and locked_until set to now plus the request deadline, and commit that reservation on its own before any work is attempted - an uncommitted row is invisible to the concurrent retry, which is the entire point of writing it first. One caller wins the insert and proceeds; the other takes the unique violation and reads the row rather than falling through to the work.
- Branch the loser on what it found. 'succeeded' replays response_status and response_body verbatim, with a header marking it a replay. 'in_flight' with locked_until in the future returns 409 and a Retry-After - do not park a request thread waiting on another request, that is how a concurrency bound is consumed by retries. 'in_flight' past locked_until is reclaimable by a conditional UPDATE predicated on the old locked_until, so exactly one reclaimer wins. A fingerprint mismatch is 422 and never the stored response: a different body under the same key is a client defect, and serving the old response hides it forever.
- Commit the effect and the key's completion together - resource insert, revision row, outbox row, and the UPDATE of the reserved key to 'succeeded' with the stored response, in one transaction. That makes a crash binary: either all of it is durable and the key reads 'succeeded', or the transaction rolls back and the only durable trace is the reservation row, still 'in_flight', with no resource behind it. The reclaim branch finishes that case - once locked_until passes, one retry takes the row by conditional UPDATE and runs the work for the first time. Splitting the two commits is what creates the window with no recovery: the effect durable, the key still 'in_flight', and a reclaimer that cannot distinguish that from work never started, so it creates a second resource.
Follow-up
- Response bodies are pruned at 24 hours but resource_id is kept. What do you return to a replay that arrives after pruning?
- The endpoint now also sends a welcome email. Which part of this contract prevents a second send, and which part cannot?
Walk me through your process for debugging a memory leak in a production environment.
Medium · Technical Foundations and Algorithms
Approach
- Confirm it really is a leak. Retained memory (heap after full GC, or RSS) should climb steadily across GC cycles and never level off. Rule out cache warm-up, legitimate load growth and fragmentation. Line the growth curve up against deploy times, traffic and particular endpoints or jobs.
- Contain the damage before you investigate. Use rolling restarts or temporary memory limits so users are not hit by out-of-memory crashes. Take one affected instance out of the load balancer so you can examine it without serving traffic.
- Capture two snapshots some time apart and diff them. Java: jcmd <pid> GC.heap_dump, then the dominator tree in Eclipse MAT. .NET: dotnet-gcdump or dotnet-dump. Python: tracemalloc snapshots compared with compare_to. Look for the types whose retained size grew, then follow the reference path from the GC root that keeps them alive.
- Check the usual causes. Unbounded caches or static collections keyed by request or user ID. Event handlers or listeners that are never unsubscribed. ThreadLocal values on pooled threads. Connections, streams or disposables that are never closed. If RSS grows while the managed heap stays flat, look at native memory instead.
- Fix it and prove the fix. Bound the cache with LRU or TTL eviction, or release resources with using or try-with-resources. Reproduce the leak under a load test and show memory staying flat after the change. Add an alert on post-GC heap growth so a regression shows up before it causes an outage.
Follow-up
- The managed heap is flat but process RSS keeps growing. What do you check next?
- Taking a heap dump pauses the process and writes everything in memory to disk. How do you do that safely in production?
- How would you catch this kind of leak before a release rather than in production?
Suggested practice checklist
The plan follows what candidates report: algorithms and data structures first, then SQL and schema design, production debugging, and finally behavioral stories and a full mock. Each day names the reported question it practises.
Day 1 — Confirm the format and pick your language
- Write the questions to send your recruiter: which stages exist, which involve live coding, whether a design or schema discussion is included, and whether the final stage is onsite or virtual
- Choose one of Java, C# or Python, the object-oriented languages candidates list, and set up an editor where you can run code and tests
- Write a one-page checklist of the topics reported: arrays and strings, linked lists and trees, hash maps, SQL, schema design and recursion
Deliverable: A recruiter question list, a chosen language with a working editor, and a one-page topic checklist
Day 2 — Arrays, strings, hash maps and sorting complexity
- Solve the pair-that-sums-to-a-target problem three ways (brute force, sort plus two pointers, hash map) and write the time and space cost of each
- Write merge sort and quicksort from memory, then list for each the worst case, the memory used and whether it is stable
- Practise answering the sorting-complexity question aloud, naming input assumptions
Deliverable: Three pair-sum solutions with costs, two sorting implementations, and a table comparing their worst case, memory and stability
Day 3 — Queues, stacks and data-structure choice
- Implement a queue three ways (linked list, circular array, two stacks) and write the cost of enqueue and dequeue for each
- Implement a stack using only queues, and write the cost of push and pop
- Answer the choose-a-structure-for-a-queue question aloud in under a minute, with one sentence on which access pattern decides it
Deliverable: Three queue implementations, one stack-from-queues implementation, and a cost table covering each operation
Day 4 — Recursion, linked lists and trees
- Write recursive tree height, in-order traversal and linked-list reversal, then trace each on empty, single-node and skewed inputs
- Rewrite the tree traversal iteratively with an explicit stack and note when the call stack would overflow
- Write a memoized recursive function (such as Fibonacci or paths in a grid) and state the before and after complexity
Deliverable: Recursive and iterative versions of a tree traversal, a memoized function with both complexities, and a list of edge cases for each
Day 5 — SQL tuning and schema design
- Design a small schema for patients, appointments and practices with primary keys, foreign keys and the indexes your main queries need
- Load sample rows, write a slow query (a filter on an unindexed column, a function applied to an indexed column), read its execution plan, then fix it and compare plans
- Answer the slow-query question aloud using that example
Deliverable: A schema with indexes, a before-and-after execution plan for one query, and a spoken answer to the slow-query question
Day 6 — Debugging a production memory leak and write-safety design
- Write out the memory-leak diagnosis as a numbered sequence: confirm the growth, find what changed, compare two heap snapshots, trace the retaining references, fix and verify
- List the three most common causes in your language (unbounded caches, unremoved listeners, unclosed resources) and how each shows up in a heap profile
Deliverable: A written leak runbook, a list of common leak causes for your language, and an outline for the retry-safe write endpoint
Day 7 — Behavioral stories and a full mock
- Write three STAR stories from your resume projects, each naming the technical hurdle you overcame and the decision you made
- Rehearse one story on working with a product manager to turn a requirement into a technical plan, and one on a code review disagreement
- Run one full mock: a coding problem aloud, a query-tuning answer and one behavioral answer, then note two questions to ask your interviewers about the team's technical challenges
Deliverable: Three written STAR stories, a recorded or timed mock covering code, SQL and behavioral answers, and two questions for your interviewers
Behavioral and situational preparation
Candidates report behavioral and situational questions about communication and handling conflict, and they are advised to use the STAR structure and to be ready to discuss resume projects in depth, including the technical hurdles. The recursion edge-case question is filed with the behavioral prompts but is answered as a technical walkthrough, so practise it both ways. The role involves working with product managers, peer code review and owning features through deployment, so choose stories that show those.
- Tell me about a project on your resume where you hit a technical hurdle, and walk through how you got past it.
- Describe a time you turned a business requirement from a product manager into a technical plan, and what you changed along the way.
- Tell me about a code review where you disagreed with the author or the reviewer, and how it ended.
- Describe a feature you owned from first requirement to deployment, and what you did to keep it reliable.
How do you handle edge cases when writing recursive functions?
Medium · Technical Foundations and Algorithms
Approach
- Open with the structure: every recursive function needs at least one base case that returns without recursing, and every recursive call must move strictly closer to one. Say how you would show termination, for example that a list gets shorter by one or a tree call goes to a child.
- List the input shapes the base case must cover before writing the recursive step: null or empty input, a single element, a negative or out-of-range parameter, and duplicates where they matter. Name them for a concrete function such as tree height or summing a linked list, and trace the smallest input by hand.
- Cover depth. Recursion uses one stack frame per level, so a skewed tree or a long linked list can overflow the stack: Python's default limit is about 1000 frames and Java throws StackOverflowError. Mention that Java, C# and Python do not optimize tail calls, so a deep case needs an explicit stack or a loop.
- Cover repeated work and cycles. Overlapping subproblems (naive Fibonacci is exponential) call for memoization or bottom-up dynamic programming, and a graph or a structure with cycles needs a visited set or the call never ends. In backtracking, undo shared mutable state after each branch, and watch for integer overflow in values such as factorials.
Follow-up
- How would you rewrite this recursive traversal iteratively with an explicit stack, and what changes in the order of visiting?
- How would you find and fix an infinite recursion you hit in production?
- When is recursion the wrong choice even though it reads more clearly?
Estimate work you have never done and defend the range
Hard · estimation · backfills · expand-contract
You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.
Approach
- Decompose into independently deployable steps before estimating anything: add the column nullable, write both shapes, backfill in batches, verify, move reads, stop writing the old shape, drop it. That is four deploys spread over days, and the calendar estimate is dominated by them rather than by the loop's runtime.
- Do the arithmetic aloud for the part that has arithmetic in it: batch size times number of batches times per-batch duration, at a write rate the primary can absorb alongside roughly 1.2k writes per second of production traffic. The loop is throttled by replication lag and lock waits, not by how fast it can issue statements.
- Price the schema step by its lock rather than its statement duration. In PostgreSQL an ALTER TABLE taking ACCESS EXCLUSIVE waits for every open transaction on that table while later queries queue behind it, so a millisecond change issued during a thirty-second analytics query stalls that table for thirty seconds. Adding a nullable column with a non-volatile default avoids a rewrite from version 11; a new index wants CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an invalid index behind if it fails.
- Express the answer as a range whose endpoints each trace to a stated assumption, then name the cheapest experiment that collapses it, which is almost always running one real batch against the real table and multiplying.
Follow-up
- How do you verify the backfill genuinely finished, given rows written by production traffic while it ran?
- Where does the backfill resume from after a worker is killed mid-batch, and what makes that resume point trustworthy?
Tell callers you do not own that their integration breaks
Medium · deprecation · compatibility · stakeholders
A field in a write endpoint's response must change shape. You own the endpoint; you do not own the four internal callers or the outbound webhook consumers who read it. Describe a deprecation you were responsible for: what you shipped first, how you established who was actually reading the field, the window you gave and what set its length, what you did about the consumer who never moved, and how you decided removal was safe. Name the signal you used, not the announcement you sent.
Approach
- Establish the reader set empirically rather than from a wiki of owners: per-field usage counters keyed by principal, or access logs attributed to a consumer. State the blind spot of whichever you pick, since a consumer that reads the field only on a monthly job will not appear in a week of logs.
- Ship additive first. Populate the new field alongside the old one so no reader is forced to move, which is also what keeps a rolling deploy safe, because old and new instances answer the same requests at the same time and a rollback must still find the old shape present.
- Set the window from the slowest legitimate consumer's release cadence, not from your calendar, and decide separately what to do for a consumer with no release process at all, such as an external webhook endpoint you can only email.
- Convert silence into evidence before you rely on it: a short, low-traffic removal window that makes a still-dependent consumer fail visibly and loudly while you are watching, rather than at three in the morning after you have moved on.
Follow-up
- How would you detect a consumer that reads the field only during a monthly export?
- One caller refuses to move and has a commercial relationship behind it. What changes in your plan and what does not?