Interview guides · BrightMoney
BrightMoney Software Engineer Interview Guide
BrightMoney Software Engineer interview guide: five reported coding questions on sliding window, two pointers and hash maps, plus a 7-day prep plan.
The 60-second brief
BrightMoney builds financial tools aimed at helping people manage debt, save money and make decisions about their finances. Candidates describe the Software Engineer role as backend work on those products: building and maintaining services, turning financial requirements into code, and keeping existing financial logic accurate and fast as the user base grows. The role description also mentions code reviews, architectural discussions and working alongside product managers and other engineers.
This guide is for candidates interviewing for a Software Engineer position at BrightMoney. It covers the five reported coding questions, the patterns behind them (two pointers, sliding window, hash maps, complexity analysis), how to confirm the loop format with your recruiter, and a seven-day plan. It makes no claims about rounds nobody has reported, such as system design or a take-home.
No round sequence is reported for this loop. Candidates report algorithmic coding questions on strings and arrays, with two pointers, sliding window and Big O analysis as the named topics.
Focus areas
- Data Structures & Algorithms (DSA)
- Problem Solving
- Two Pointers Technique
Overview
The reported material is narrow, which makes it easy to prepare for well. All five questions sit in one family: contiguous substrings and subarrays, solved by moving two indices through the input, sometimes with a hash map recording what the window holds. Candidates name two pointers, sliding window and complexity analysis as the topics, and the example problems they cite are medium-level: the longest palindromic substring, and the maximum sum of a subarray of size K.
Treat the family as one skill with three decisions. First, is the window fixed in size or does it grow and shrink? Maximum sum of size K is fixed; the longest substring without repeating characters and the smallest subarray with a sum of at least a target are variable. Second, what state does the window carry: a running sum, a set of characters, or a map from character to last index? Third, what makes the window invalid, and which pointer moves when it is? If you can answer those three for a new problem, you can write the loop.
Know where each pattern stops working. The shrinking-window solution for the smallest subarray with a sum of at least a target depends on every element being non-negative; with negative values you need prefix sums and a monotonic deque instead. The two-pointer approach on a sorted array depends on the sort order giving you a reason to move one end. Naming these limits out loud is part of showing you chose the technique rather than recalled it.
The role description mentions backend services, financial logic that must stay accurate, and growing user volume. For the coding questions, that translates into habits you can practise: state time and space complexity for every solution, test empty input, a single element, negative values and large arrays, and explain your plan before you write code. Candidates also advise treating the session as a conversation: ask about input sizes and output format first.
Where to focus your preparation
No stages are reported for this loop, so there is a single preparation entry below. Confirm the real sequence with your recruiter and work the five reported questions in the meantime.
Preparation focus
Candidates do not report a round sequence, round names or stage lengths for the BrightMoney Software Engineer loop, so this entry covers what is reported: five algorithmic coding questions and the topics around them (two pointers, sliding window, complexity analysis). Ask your recruiter how many stages there are, which language you may use, and whether you will code in an editor or on a whiteboard. The role requirements candidates describe list Java, Python or Go as examples, so pick the one you can write most fluently under pressure and prepare in it.
What to demonstrate
- Working code for substring and subarray problems using two pointers or a sliding window
- Time and space complexity stated and justified for each solution
- Edge cases handled: empty input, large arrays, negative values
- Clear explanation of the approach before and during coding
How to prepare
- Solve the longest substring without repeating characters question two ways: a set-based window and a last-seen-index map
- Write the smallest subarray with a sum of at least a target solution, then state what breaks if the array has negative numbers
- Work a sorted-array problem with converging pointers, then explain why the sort order justifies each pointer move
- Practise the amortized argument for sliding window complexity out loud, and take a brute-force solution to a hash map or window version
Where candidates lose points
Using a shrinking window for the smallest subarray with a target sum when the array may hold negative numbers
Ask whether values can be negative before choosing a technique. With non-negative values, the window moves right and shrinks from the left while the sum meets the target. With negatives, switch to prefix sums with a monotonic deque, or say plainly that the window no longer applies.
Moving the left pointer backwards in the longest-substring solution when you store each character's last index
Set left to the larger of its current value and the last index plus one. Test with an input where a repeated character falls outside the current window, such as abba, where a missing max returns the wrong length.
Calling a sliding window O(n squared) because a while loop sits inside a for loop, or claiming O(1) space with a map
Give the amortized argument: the right pointer advances at most n times and the left pointer at most n times, so total work is O(n). State the space as the size of the window state, for example O(min(n, alphabet size)) for distinct characters.
Starting to code before clarifying inputs, which candidates report as a common reason for struggling in the coding round
Open with questions: can the input be empty, how large can it get, are values negative, should a missing answer return 0, -1 or null, and what counts as a character? Then state brute force, its complexity, and the optimization you plan.
Off-by-one errors in window length and unchecked boundary cases
Define the window as inclusive of both ends and compute length as right minus left plus one. Dry-run on empty input, a single element, k larger than the array, a target no subarray reaches, and an entire array that qualifies before you declare the solution done.
Questions & practice
How would you implement a solution to find the longest substring without repeating characters?
Medium · Algorithmic Coding & Data Structures
Approach
- Ask about the character set first: lowercase ASCII lets me use a fixed int array of size 128 (or 26) indexed by character, while arbitrary Unicode needs a hash map. Then name the brute force: check every substring with a set, which is O(n^2) with an incremental set per start index, or O(n^3) if each substring is rechecked from scratch.
- Use a sliding window with a last-seen map: for each right index r, if s[r] was last seen at an index >= left, move left to lastSeen[s[r]] + 1; then record lastSeen[s[r]] = r and best = max(best, r - left + 1).
- Guard against left moving backwards ("abba": at the second 'a', its last index 0 is already left of the window), so write left = max(left, lastSeen[c] + 1) or check lastSeen[c] >= left before jumping.
- Complexity: O(n) time because each index is visited once by the right pointer and left only jumps forward; O(min(n, alphabet size)) space for the map. The set-based variant (remove s[left] while s[r] is in the set) is also O(n), since each character is added and removed at most once.
- Test the edge cases out loud: empty string returns 0, a single repeated character ("bbbb") returns 1, all-distinct input returns n, and a repeat at the very end of the string.
Follow-up
- Return the substring itself rather than its length: what extra state do you track?
- Generalise to the longest substring with at most k distinct characters: how does the window state and shrink condition change?
- If the string arrives as a stream too large to hold in memory, what do you keep, and does the answer change?
Given an array of integers, how do you find the smallest subarray with a sum greater than or equal to a target
Medium · Algorithmic Coding & Data Structures
Given an array of integers, how do you find the smallest subarray with a sum greater than or equal to a target value?
Approach
- Clarify the sign of the values first. If every element is positive, extending the window only increases the sum and shrinking only decreases it, which is the monotonicity a sliding window needs. Ask what to return when no subarray qualifies (usually 0).
- Variable-size window: move right, add a[right] to the sum; while sum >= target, record right - left + 1 as a candidate minimum, subtract a[left] and advance left. O(n) time, O(1) extra space, because each pointer moves at most n times.
- Mention the brute force (all start/end pairs with a running sum, O(n^2)) and the middle option: with positive values, prefix sums are strictly increasing, so for each start I can binary search the first prefix >= P[i] + target, giving O(n log n).
- If negatives are allowed, the window breaks because dropping the left element can raise the sum. The O(n) fix is prefix sums plus a deque of indices with increasing prefix values: pop the front while P[j] - P[front] >= target (recording j - front), pop the back while P[j] <= P[back], then push j.
- Edge cases: empty array, no qualifying subarray, a single element already >= target (answer 1), target <= 0 (clarify whether that is allowed), and integer overflow on the running sum in Java or Go, so use a 64-bit accumulator.
Follow-up
- Return the start and end indices of the shortest subarray instead of its length.
- How does your solution change if the array can contain negative numbers?
- Count the subarrays whose sum is exactly equal to the target instead: which technique replaces the window?
Explain your approach to using the two-pointer technique to solve a sorted array problem.
Medium · Algorithmic Coding & Data Structures
Approach
- Name the pattern families: opposite-end pointers (pair with a given sum, 3Sum, container with most water), same-direction reader/writer pointers (remove duplicates in place, move zeros), and one pointer per array (merging two sorted arrays, intersections).
- Walk through pair-sum on a sorted array: start left = 0, right = n - 1; if a[left] + a[right] < target, increment left, if greater, decrement right, stop when left meets right. State the invariant: when the sum is too small, a[left] cannot pair with any element at or below a[right], so discarding it loses no answer.
- Complexity: O(n) time and O(1) extra space for one pass, compared with O(n) space for a hash set. If the input is not already sorted, sorting adds O(n log n) and loses the original indices, so keep (value, index) pairs if indices are required.
- Extend to 3Sum: sort, fix each i, run the pair search on the suffix, and skip equal neighbours for i, left and right to avoid duplicate triples. That gives O(n^2) time, better than the O(n^3) triple loop.
- Edge cases: fewer than two elements, duplicates, all-negative or all-equal arrays, the loop condition left < right so an element is not paired with itself, and overflow when adding two large values.
Follow-up
- Prove why moving the left pointer when the sum is too small never skips a valid pair.
- Count all pairs with sum less than the target in one pass. Hint: when a[left] + a[right] < target, every index between left and right pairs with left, so r - l pairs are added at once.
- Find the longest palindromic substring with pointers expanding from each centre: how many centres are there, and what is the complexity?
How do you determine the time and space complexity of a sliding window algorithm?
Medium · Algorithmic Coding & Data Structures
Approach
- Use an amortised argument, not a per-iteration one: the right pointer advances n times and the left pointer only moves forward, so across the whole run it also moves at most n times. The inner while loop therefore does at most n total iterations, and total pointer work is O(n), not O(n^2).
- Multiply by the cost of each window update: a running sum or a fixed 26-slot count array is O(1); a hash map is expected O(1) per operation; maintaining a window maximum with a monotonic deque is amortised O(1), while a heap with lazy deletion or a balanced tree (TreeMap) costs O(log k), giving O(n log k).
- Contrast fixed and variable windows: the maximum sum of a size-K subarray is O(n) with add-the-new, subtract-the-old updates, against O(nK) if each window is summed from scratch.
- Space is whatever the window state holds: O(1) for a sum, O(min(n, alphabet)) for character counts, O(k) for a deque or a map of k distinct keys. Say whether output is counted, since returning every window's result is O(n - k + 1) extra.
- Call out hidden costs that break the bound: slicing a substring or calling sum() on a list slice inside the loop copies the window and makes it O(nk); comparing two full count maps on every step adds a factor of the alphabet size.
Follow-up
- Why does the nested while loop in a variable-size window still give linear time? Count the total number of left-pointer moves.
- What is the complexity of the sliding window maximum with a deque, and why is each index pushed and popped at most once?
- What happens to your bound if the hash map degrades under many collisions?
Can you optimize a brute-force approach for a sequence-based problem using a hash map or a sliding window?
Medium · Algorithmic Coding & Data Structures
Approach
- Start from the brute force and its cost, then name the repeated work: a nested loop that searches for a complement or recomputes a range sum is the signal. Cache that work in a hash map keyed by value or prefix sum, or reuse it with a window that adds one element and removes one.
- Pick the tool from the problem's properties: a contiguous range with a condition that stays monotone as the window grows (non-negative values, or 'at most k distinct') fits a sliding window. Pair or complement lookups, or sums with negatives, fit a hash map.
- Worked example 1: two-sum on an unsorted array goes from O(n^2) pairs to one pass storing value -> index, checking target - x before inserting x so an element is never paired with itself. O(n) expected time, O(n) space.
- Worked example 2: counting subarrays with sum exactly k when negatives exist. A window fails because the sum is not monotone. Instead keep a map of prefix-sum counts initialised with {0: 1}, and for each running sum p add count[p - k]. O(n) time, O(n) space.
- State the trade-off: the hash map spends O(n) memory to remove a factor of n, and its O(1) is expected rather than worst case. Sorting plus two pointers is the O(n log n) time, low-memory alternative when original order or indices do not matter.
Follow-up
- Memory is tight and the input is too large for an O(n) map: what do you give up, and what does sorting with two pointers cost instead?
- Find all anagrams of a short pattern in a long string: how does a fixed-size window with a 26-slot count array keep it linear?
- Find the longest run of consecutive integers in an unsorted array in O(n): why do you only start counting from numbers whose predecessor is missing?
Denormalise tenant onto revisions and backfill it live
Hard · denormalisation · online migration · backfill
resource_revision (revision_id, resource_id, version, actor_user_id, change_kind, patch, request_id, created_at) has 400M rows and no tenant column; tenant_id lives only on resource. Two reads need it: a tenant-scoped audit feed ordered by created_at DESC, and an offboarding purge. Both join back to resource today. Justify adding tenant_id to resource_revision against those two reads, name the anomaly the copy introduces and the constraint that prevents it, then give the ordered migration for a live table taking 1.2k writes/second — the lock each step takes, how the backfill is batched, and where each step stops being reversible. PostgreSQL 16.
Approach
- Justify from the access path rather than from taste. Without the column, the audit feed either scans resource_revision by created_at and discards other tenants' rows, or resolves the tenant's resource_ids first and probes with them — both proportional to the tenant's whole history rather than to one page. With (tenant_id, created_at DESC, revision_id DESC) it is a seek that stops at 50 rows, and the purge becomes a ranged delete instead of a join.
- Name the cost exactly: a second copy of a fact can disagree with the first. Make the disagreement unwritable rather than documented — add UNIQUE (resource_id, tenant_id) on resource so it can serve as a foreign-key target, then FOREIGN KEY (resource_id, tenant_id) REFERENCES resource (resource_id, tenant_id) on the revision table. A revision can then only ever carry its parent's tenant.
- Step one, expand: ALTER TABLE resource_revision ADD COLUMN tenant_id BIGINT NULL, with no default, so it is a catalogue change and no rewrite. It still needs ACCESS EXCLUSIVE for an instant, and that instant queues behind the longest open transaction on the table while every later query queues behind it — set lock_timeout to 2s and retry rather than wait.
- Step two, dual-write: deploy the writer that populates tenant_id on every new revision while reads still use the join. Reversible by redeploying the previous build, because nothing reads the column yet.
Follow-up
- The backfill is half finished and a rollback is required. What state is the table in, and what does the previous build do with a half-populated column?
- How do you verify the backfill actually finished, given rows are still being inserted while it runs?
Keep soft-deleted accounts from blocking re-registration
Easy · soft delete · unique constraints · engine differences
app_user holds user_id, tenant_id, email CITEXT, password_hash (NULL for SSO principals), email_verified_at, auth_version, status ('invited','active','suspended','deactivated'), created_at, updated_at, deleted_at. Two live accounts for one address inside a tenant must be impossible, but an address freed by a soft delete must be reusable, and the same tenant may delete and re-register it repeatedly. Write the uniqueness DDL for PostgreSQL 16, then the equivalent for MySQL 8 where partial indexes do not exist, and say what each permits once three deleted rows already hold that address.
Approach
- Start from what is actually unique: not (tenant_id, email), but (tenant_id, email) among live rows. PostgreSQL says that directly — CREATE UNIQUE INDEX app_user_live_email ON app_user (tenant_id, email) WHERE deleted_at IS NULL. A full constraint over the same two columns burns the address permanently the first time someone deletes an account.
- Keep case-insensitivity in the type or the index, never in the application: CITEXT as given, or UNIQUE (tenant_id, lower(email)) as an expression index where the extension is unavailable. A case-sensitive unique column is exactly how two accounts for one human appear.
- For MySQL 8 the predicate has to move inside the key: add a discriminator column that is a constant 0 while the row is live and is set to user_id on delete, with UNIQUE (tenant_id, email, deleted_marker). Live rows share the constant and still collide; deleted rows differ from each other and stop colliding.
- State the NULL variant and its dependency: leaving the marker NULL for deleted rows also works, because a unique index treats NULLs as distinct — true in MySQL, and true in PostgreSQL only under the default NULLS DISTINCT, which PostgreSQL 15 lets you reverse. Check the polarity against the three existing deleted rows: constant-on-live is what preserves the collision you want, and reversing it silently admits duplicate live accounts.
Follow-up
- A deleted account re-registers with the same address the next day. Do the old resource rows follow the new user_id, and how does the API keep the two principals apart?
- How do you honour an erasure request while resource_revision.actor_user_id still references this table?
Publish rate-limit and deadline semantics the edge actually enforces
Easy · rate limiting · backpressure · retries
The edge API serves about 3k requests/second steady and 9k at peak against a 400 ms p99 budget, with an explicit bounded concurrency limit per instance. Limits exist per principal and per tenant. Callers are a partner integration running nightly bulk loads and a browser app. Specify the counting algorithm and window, which limit a request is charged against, the headers a well-behaved client reads, the status and body when a limit is hit, how that differs from the response when an instance is shedding load, and what each caller does with each.
Approach
- Choose the counter and name its failure mode. Fixed windows admit nearly twice the limit across a boundary - a full burst at the end of one window and another at the start of the next. A token bucket states sustained rate and burst separately, which is exactly what a nightly bulk load needs. A sliding-window counter is more faithful and costs more state per key. State the choice and the burst it permits.
- Charge each request against both keys and reject on the stricter. The tenant limit protects the shared primary, which absorbs roughly 1.2k writes/second in total; the per-principal limit stops one credential inside a tenant from consuming that tenant's whole allowance. The tenant is the fairness unit for the same reason it is the leading column of every index.
- Advertise limit, remaining and reset for the binding key on every response, not only on rejections, so a client can pace before it is refused. Pick one naming scheme - the RateLimit-* draft fields or an X-prefixed set - document the units, and never change them afterwards.
- Separate two rejections that look identical to a naive client. 429 means this caller exceeded its own share and Retry-After is a real schedule it should obey. 503 means the instance is at its concurrency bound and shedding, which is a statement about the server; a fleet-wide 503 retried on a fixed delay resynchronises every client into one stampede, so full jitter is mandatory there and the delay is the client's guess, not ours.
Follow-up
- One tenant stays under its limit and still degrades everyone else during a backfill. What changes - the limiter, the worker concurrency caps, or both?
- How are counters kept correct across 20 to 40 stateless instances, and what does your answer cost per request?
Cache the tenant listing feed with a bounded staleness window
Easy · caching · cache invalidation · keyset pagination
GET /v1/resources returns one tenant's resources ordered by updated_at DESC, 20 per page, at 14k requests/second peak against a 120 ms p99. The table carries the index (tenant_id, status, updated_at DESC, resource_id DESC) and writes land on the primary at 1.2k/second. Design the read path: the pagination contract, the cache key and value, what a write invalidates, and the staleness a user can observe. State the request rate that actually reaches the database, and the one repopulation race that deleting on write does not close.
Approach
- Settle the pagination contract first, because it decides what is cacheable. OFFSET makes the database produce and discard the skipped rows, so page 500 costs five hundred pages of work, and rows inserted between two fetches shift across the boundary and are skipped or repeated with nothing in the response to reveal it. The cursor is the row value of the last row returned: WHERE tenant_id = $1 AND status = $2 AND (updated_at, resource_id) < ($3, $4) ORDER BY updated_at DESC, resource_id DESC LIMIT 21. That is a row-value comparison, not updated_at < $3 AND resource_id < $4, which is a different and wrong predicate.
- Confirm the index actually serves it: equality on the two leading columns, then a range on the pair that follows in exactly the index's sort order, so the plan is an index scan that touches 21 entries with no sort node. Requesting 21 to return 20 is how has_more is answered without a count. resource_id is not decoration - updated_at is not unique, and without the tie-break two rows sharing a timestamp at a page boundary are the skip that keyset pagination was adopted to remove.
- Key the cache on every value the predicate reads: tenant_id, status, cursor and page size. A key that omits tenant_id is a cross-tenant disclosure, and no test running against a single tenant's data will show it.
- Be honest that a write does not invalidate one key. An update moves its row to the head of the ordering, so it invalidates the first page and every cursor page whose range spans the row's old and new position, which is not enumerable. Cache the first page per (tenant_id, status) - that is where the traffic is - with a short TTL, invalidate it on write, and serve deep cursor pages uncached from a replica, since each is already a 21-row index scan and they are rare.
Follow-up
- The tenant writes and immediately lists. What does it see, and what is the smallest change that makes its own write visible without sending all 14k requests/second to the primary?
- A tenant has 4 million resources and a client walks every page nightly. What does that do to the cache hit rate, and should that traffic share this path at all?
Exports duplicate a row range about once a week
Hard · leases · clock skew · idempotency
Roughly once a week an export writes a file containing a duplicated range of rows. The affected job_run rows show attempt = 1, status = succeeded, one started_at, and a lease_owner naming a different host from the one whose logs show the job starting. Leases last 30 seconds and are heartbeated every 10 from inside the handler; lease_expires_at is computed on the worker and compared against the database's now(). Find the mechanism, and give a fix that holds even if you cannot fix the clocks.
Approach
- Start from the fact that eliminates the obvious answer. attempt = 1 means no retry was recorded, so this is not a re-run after failure; two workers ran the same row concurrently and the takeover path never touched the counter. lease_owner naming a host other than the one that started the job is the same statement from the other side.
- Enumerate the mechanisms that cause a premature takeover, then find the signal that separates them. Either the lease genuinely expired because the heartbeat did not fire, which is what happens when the heartbeat runs on the handler's own thread and the handler makes a long blocking call, or it only appeared expired because two clocks disagree, since lease_expires_at is written from the worker's clock and evaluated against the database's. The discriminator is the distribution: incidents clustered on the longest exports indict the heartbeat, incidents clustered on one host indict skew. Measure both, and measure each host's offset against the database directly.
- Read the reclaim query precisely. In PostgreSQL now() is transaction start time, not statement time, so a reclaimer holding a long transaction compares against an older timestamp than expected; clock_timestamp() is the statement-time function. This is worth ruling in or out before you redesign anything, because it changes which rows look expired.
- Remove the second clock rather than trying to synchronise it. Issue and extend the lease in the database, with lease_expires_at = now() + interval '30 seconds' in both the claim and the heartbeat, so exactly one clock is ever compared and worker skew stops mattering to this predicate.
Follow-up
- The displaced worker has already streamed half the file to object storage. What makes that side effect safe to repeat?
- You now count takeovers. What alert fires on that counter, and at what threshold?
Suggested practice checklist
The seven days start by confirming the format, then move through two pointers, fixed and variable windows, the hash-map version, and complexity analysis, using the five reported questions as the core practice. The last two days cover communication and a full timed run.
Day 1 — Confirm the format and set up your language
- Email your recruiter to ask for the stages, whether you will code in an editor or on a whiteboard, and which languages are allowed
- Pick one of Java, Python or Go and write from memory a loop template with left and right indices and a window-state variable
Deliverable: A recruiter reply (or a list of open questions if none yet) and one working window-loop template in your chosen language.
Day 2 — Two pointers on sorted arrays
- Solve a pair-with-target-sum problem on a sorted array with converging pointers, then remove duplicates in place with a slow and fast pointer
- Write the longest palindromic substring with expand-around-center, and state its O(n squared) time and O(1) space
- Explain out loud why sort order justifies moving the left or the right pointer
Deliverable: Three solved problems with complexity written beside each, plus a spoken explanation of the pointer-move rule for the sorted-array question.
Day 3 — Fixed and variable windows on arrays
- Implement the maximum sum of a subarray of size K by adding the entering element and subtracting the leaving one
- Implement the smallest subarray with a sum of at least a target with a shrinking window, returning 0 when none exists
- Write down what changes with negative numbers and sketch the prefix-sum and monotonic-deque alternative
Deliverable: Two working solutions with a table of edge-case tests each, and a short note on the negative-number limit.
Day 4 — Longest substring and the hash-map window
- Solve the longest substring without repeating characters with a set and two pointers, then again with a map of last-seen indices that jumps the left pointer
- Test both on abba, an empty string, a single character and all-unique input
- Take one brute-force substring problem, state its cost, and rewrite it with a hash map or a window
Deliverable: Two solutions to the longest-substring question, a passing test list, and one brute-force-to-optimized rewrite with before and after complexity.
Day 5 — Complexity analysis out loud
- For every solution from days 2 to 4, write the time and space complexity with a one-sentence justification
- Practise the amortized argument for sliding window aloud until you can give it without notes
- Answer the sliding window complexity question in two minutes of speech, recorded, and listen for hedging or skipped steps
Deliverable: A one-page complexity sheet for all solved problems and a recorded spoken answer to the sliding window complexity question.
Day 6 — Communication and working-style stories
- Prepare two stories each from your own work: optimizing existing logic without changing its output, and handling a disagreement in code review
- Practise explaining a technical trade-off to a non-engineer in plain language
Deliverable: Four written stories with a result for each and one recorded plain-language trade-off explanation.
Day 7 — Full timed run
- Pick two of the five reported questions you have not seen for two days and solve them aloud, in your chosen language, from clarifying questions through testing
- Review where you rushed, skipped an edge case or lost the complexity argument
- Write two questions to ask your interviewers about the team and its backend work
Deliverable: Two timed spoken solutions with a short list of fixes, and two written questions for interviewers.
Behavioural preparation
No behavioral round is reported for this loop. The role description candidates share mentions code reviews, architectural discussions and working with product managers, and candidates stress explaining your reasoning, so prepare stories on those topics in case conversation questions come up.
- Describe a time you optimized code that was already correct. What did you measure, what did you change, and how did you confirm the results stayed identical?
- Tell me about a code review where you disagreed with a teammate on an approach. What did you write, and how was it settled?
- Walk me through a problem you broke into smaller, testable pieces before writing any code.
- Tell me about a bug caused by an unhandled edge case, such as empty input or an unexpected negative value. How did you find it and what did you change afterwards?
- Describe a time you explained a technical trade-off to a product manager who had to make a decision.
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?
Turn a code review disagreement into a decision
Easy · code review · optimistic concurrency · communication
A colleague's change updates a row with UPDATE resource SET version = version + 1 WHERE resource_id = $1 AND version = $2 and treats an affected-row count of zero as a successful no-op. You read that as a silently lost update; they think returning 200 is friendlier to clients than returning a conflict. Describe how you have handled a review disagreement of this shape: what goes in the comment, when you leave the thread, and who decides. Then write the comment you would leave here, in under 80 words.
Approach
- Sort the disagreement before writing anything. A silently discarded write is a correctness claim about data; the choice between 409 and 412 is taste. Only the first justifies blocking a merge, and saying which one you are doing is most of the value of the comment.
- Make the claim reproducible in the comment itself with an interleaving rather than a principle: A reads version 7, B reads version 7, B commits version 8, A's predicate matches zero rows, A is told it succeeded and A's edit is gone.
- Offer the alternative with its cost attached: return 409 carrying the current version and the revision that won, so the client can re-read and re-apply. Note that automatic retry is not the fix, because a retry re-reads the winner's state and reapplies an intent formed against data that no longer exists.
- Apply an escalation rule you can state: two round trips on the thread, then a call, and the service's owner decides rather than the reviewer. A reviewer who cannot be overruled is a bottleneck with extra steps.
Follow-up
- Where would you put the test that fails if someone reintroduces the swallowed zero rowcount?
- The author says clients cannot handle a 409. How do you check whether that is true?
Reverse your own decision and price the reversal
Medium · reversibility · measurement · migrations
Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.
Approach
- State the original rationale without irony, in the version you would still defend given what was known then. If it is not defensible, the story is about carelessness rather than judgement, and a different example serves you better.
- Give the measurement that moved with a before and after: the p99 that did not improve, the cache hit rate that sat at 40%, the plan that flipped to a sequential scan once the table passed a size you can name.
- Cost the reversal in steps, not adjectives: expand-and-contract deploys, the dual-write window, the callers who had to be notified, the rows already written in the wrong shape that had to be backfilled or abandoned.
- Distinguish reversal from rewrite by naming what you kept. Most good reversals preserve the schema or the interface and undo one decision inside it, which is also why they were affordable.
Follow-up
- What in that decision was irreversible, and did you know it was irreversible when you made it?
- How did you tell the people who had already built on top of the original decision?