Interview guides · Remitly
Remitly Software Engineer Interview Guide
Remitly Software Engineer interview prep: recruiter screen, technical assessment, onsite, reported coding, API and design questions, and a 7-day plan.
Interviewing at Remitly
- Practice bank: 12+ questions
- Rounds: 2
- Typical prep: 1–2 weeks
- Interview reports: 7
Interview rounds
| Round | Questions |
|---|---|
| Technical Screen | 5 |
| Onsite | 7 |
How hard is the Remitly Software Engineer interview?
| Difficulty | Share | Questions |
|---|---|---|
| Easy | 42% | 5 |
| Medium | 42% | 5 |
| Hard | 17% | 2 |
What Remitly actually tests for
| Topic | Share | Questions |
|---|---|---|
| Coding & Algorithms | 42% | 5 |
| Behavioral & Leadership | 25% | 3 |
| System Design | 25% | 3 |
| Software Engineering Fundamentals | 8% | 1 |
The 60-second brief
Remitly runs cross-border money transfers, and its engineering teams work on transfer processing, fraud detection, regulatory compliance and ledger management. The source notes name domain areas including Core Transfer Experience, Trust Intelligence Platform, High Value Send, Decision Intelligence Platform and Receiver Products. For interview prep, that domain matters because many reported questions (ledger APIs, exactly-once event processing, FX pricing, fraud scoring) are framed around moving money correctly under concurrency and unreliable networks.
Remitly candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
What a Software Engineer does at Remitly
The reported questions follow that domain closely. Coding questions include a TTL cache with memory limits, finding sub-arrays of transaction records that meet a threshold, and BFS/DFS over transfer paths. API questions ask for a banking endpoint that handles concurrent deposits and withdrawals, a thread-safe structure for exchange operations, a rate limiter, and a live code review that fixes concurrency bugs. Design questions cover an international transfer platform, an event-driven ledger with exactly-once processing, real-time fraud detection, FX rate calculation and receiver notifications.
In practice, clean algorithm answers are only part of it. Across coding, API and design questions, prepare to say what happens on a retry, a timeout, a duplicate message or two concurrent writes to one balance. Candidates also report behavioral questions inside technical rounds, not only in a separate conversation, so plan to switch from code to a short structured story within the same session.
The Remitly interview process
Remitly candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Recruiter Screen
Candidates describe this as a first conversation about your background and fit for the role. The source notes that the order varies by team and location: some candidates start with a timed technical assessment, and others have a recruiter screen followed by a technical phone screen with an engineer. Use this call to find out which path you are on, whether a take-home is involved, and which domain area the team sits in. A team working on transfers, trust and fraud, or receiver products points you toward different design practice. The person on the call usually does not evaluate code, so give each project a clear shape: what was breaking, what you changed and what happened after. Describe it so a non-engineer could repeat it accurately.
What it evaluates
- Whether your project summaries still hold up when a non-engineer paraphrases them for a hiring manager
- Whether you can tie past work to the problems this role involves, such as transaction correctness, concurrency or third-party integrations, without overstating fintech experience you do not have
- Whether you can separate what you did from what the team did on shared projects
How to prepare
- Write two-sentence versions of your top three projects with no internal codenames, and attach one measured result to each, or say plainly that it was not measured
- Prepare questions for the recruiter: which assessment format comes next, which languages are accepted, whether a take-home is involved, and which team or domain area the role supports
- Pick one story about correctness under concurrency or failure (a race, a duplicate, a retry that went wrong) and keep it ready, since it fits both this call and later behavioral questions
Technical Assessment
The source describes this stage as a technical assessment or phone screen that evaluates coding. It also says screening rounds often pair an algorithmic or API task with behavioral questions. Reported coding questions include finding sub-arrays of transaction records that meet a threshold, a cache with TTL and memory limits, BFS/DFS over dependency or transfer paths, array and string problems under memory constraints, and parsing log streams to aggregate metrics. The source describes some problems as evolutionary: a base version comes first, then more constraints are added. Finish a correct base version and keep the part most likely to change, such as the eviction rule or the threshold check, in one place so the next constraint is a small edit. When output is wrong, isolate the smallest failing input and trace it by hand before changing code.
What it evaluates
- Whether you produce runnable code with its corner cases handled (empty input, duplicates, equal timestamps, expired entries) and state the time and space cost
- Whether you can extend a working solution when a new constraint arrives without rewriting it
- Whether a failing case is isolated and explained before any edit is made
- Whether you can move from code to a concise behavioral answer in the same session
How to prepare
- Implement an LRU cache (hash map plus doubly linked list) from an empty file, then add per-entry TTL with lazy expiry, then a memory cap, treating each step as the next constraint an interviewer might add
- Drill hash map and sliding-window problems on transaction-like arrays, plus bank questions such as O(1) insert/delete/random, isomorphic strings and weighted random pick
- Write a small parser that reads a log stream line by line, skips malformed records without crashing and aggregates counts per key, then state its memory use
- Prepare two short STAR stories you can give right after solving a problem
Virtual Onsite Loop
Candidates call this the Power Day. The source reports four to five sessions covering data structures and algorithms, system architecture, a deep dive into past technical work with hiring managers, and a product alignment discussion with product managers or cross-functional leads. Behavioral questions are reported inside the technical sessions as well. In some international locations (the source mentions Poland), candidates may receive a take-home task to build an API for a banking or transaction platform, followed by an onsite code review. Treat each session as a different format. Coding rewards a finished, tested solution. Design rewards agreeing on requirements, consistency and failure modes before drawing boxes. The deep dive rewards being able to defend your own trade-offs. The product session rewards being able to explain a technical constraint in terms of what it means for the user.
What it evaluates
- Whether a money-movement design states its consistency needs, idempotency and retry behaviour, and what happens when a downstream partner or queue fails
- Whether you can review unfamiliar concurrent code, name the specific race or bottleneck and fix it without adding locks everywhere
- Whether you can whiteboard a past project and defend the alternatives you rejected
- Whether you can work through a disagreement over priorities with a product partner and say what you would give up
How to prepare
- Run design reps on the reported shapes: transfer platform, event-driven ledger with exactly-once processing, real-time fraud scoring, FX rate calculation and cross-region receiver notifications
- Draw one past project from memory: components, data flow, the failure you handled, and two alternatives you rejected and why
- Practise a code review: take a multi-threaded snippet with a check-then-act on shared state, find the race, and explain the fix and its cost
- Prepare for the product session with a story about pushing back on a requirement for technical reasons and how you reached agreement
Questions Remitly candidates report
Data structures and algorithms
State the bound, then read your own code against it, because the cost you forgot is usually inside a call you did not write. A membership test against a list is linear while the same test against a set is expected constant, so one of those inside a loop turns a linear scan quadratic. Taking a slice copies the elements wherever slices are values rather than views. Repeated string concatenation is quadratic wherever strings are immutable, unless you accumulate into a builder or a list and join once. None of that is visible in the shape of the algorithm, and all of it is visible in the runtime.
- Given an array of transaction records, find all sub-arrays that meet specific threshold conditions.
- Implement a custom caching mechanism with TTL (Time-to-Live) constraints and memory limits.
- Solve array and string manipulation problems under strict runtime memory constraints.
System design and architecture
A design is a set of promises about what happens when something is slow, down, or delivered twice, and that is the part usually left unsaid. For each dependency, say what the system does when it is unavailable: serve stale data, reject the write, or accept it and settle later. For each relaxation of consistency, describe the anomaly a user would actually notice, in the product's own words, rather than naming a consistency model and moving on. The separator is volunteering the failure behaviour as part of the design instead of producing it only when pushed.
- Design an international money transfer platform handling high concurrency and strict consistency requirements.
- Structure an event-driven ledger service that guarantees exactly-once processing across unreliable network boundaries.
- Architect a distributed fraud detection pipeline capable of evaluating transactions in real time.
API and integration design
These questions are about time: a list is read while it is being written, and a contract is used by callers you cannot redeploy. For listing, state the guarantee before picking the mechanism. Offset paging over a changing table skips and repeats rows, because page two is computed against a table that page one no longer matches. Keyset paging fixes that, provided the sort key is a total order, which usually means appending a unique tiebreaker. For versioning, the separation that matters is additive against breaking, and the fact that a field, once published, cannot be unpublished on your schedule.
- Refactor a provided codebase during a live code review round to fix subtle concurrency bugs and performance bottlenecks.
- Design a high-frequency banking API endpoint capable of processing concurrent ledger deposits and withdrawals.
Behavioural and engineering judgement
Ownership questions here usually point at something with no owner and no deadline: an alert people mute, a suite everyone reruns until it passes, a service whose on-call is a rota of volunteers. Nothing forces action, so what you did says how you spend discretionary time. The strongest answers are not the ones where you fixed everything. They are the ones where you sized the problem first, chose between fixing it, containing it and writing it down for whoever comes next, told someone who could have overruled you, and can still say why the other two options were worse.
- Describe a situation where you had to make a complex technical decision with incomplete information.
Practice exercises with worked solutions
The prompts above tell you what gets asked.
Write the update path that detects a concurrent edit
resource carries version INT NOT NULL DEFAULT 1. resource_revision holds revision_id, resource_id, version, actor_user_id, change_kind, patch JSONB, request_id, created_at with UNIQUE (resource_id, version). outbox_event holds aggregate_type, aggregate_id, aggregate_version, event_type, payload, status. A PUT carries the version the client read. Write the exact statements for the single transaction that applies the edit, records the revision and enqueues 'resource.updated', and give the handler's branch on zero affected rows. Then say what PostgreSQL 16 does under READ COMMITTED when two of these updates hit one row at once.
How a strong candidate works it
- One transaction, three writes, no network call inside it: UPDATE resource SET title = $3, version = version + 1, updated_at = now() WHERE resource_id = $1 AND tenant_id = $4 AND version = $2; then INSERT the resource_revision row at version $2 + 1; then INSERT the outbox_event row at the same aggregate_version. The event goes to a table rather than a broker because no transaction spans both.
- Branch on the affected-row count before doing anything else. Zero has three causes — stale version, wrong tenant, row gone — so re-read once and map to 409 carrying the current version, or 404 for an id outside the caller's tenant, which also stops the endpoint confirming that another tenant's id exists.
- State the engine behaviour instead of assuming it. Under READ COMMITTED the second UPDATE blocks on the row lock, and when the first commits PostgreSQL re-evaluates the WHERE clause against the newly committed row, so the version predicate now fails and the statement reports zero rows. Under REPEATABLE READ the identical collision raises SQLSTATE 40001 instead, so the handler must fold both shapes into one conflict response.
- Keep UNIQUE (resource_id, version) even though the predicate already serialises writers. It is what makes a lost update unwritable if any other path ever reaches the revision table, and it converts a logic bug into 23505 rather than into a silently missing history row.
- Refuse to auto-retry the whole PUT. A retry re-reads the winner's state and reapplies an intent formed against data that no longer exists — the silent overwrite the version token was added to detect. Return the conflict; merge field-wise only if the patches are provably disjoint.
- Note that now() is the transaction timestamp in PostgreSQL, so resource.updated_at, the revision's created_at and the outbox row share one instant, which is what later makes reconciliation between the three tables unambiguous.
Follow-ups
- A client sends the version it read ten minutes ago and the resource has moved three versions. What is in your 409 so it can resolve the conflict without a full re-fetch?
- Two editors, two disjoint fields, no overlap. Does your answer still refuse the second write, and should it?
- Every write now touches a second hot table. How do you keep the outbox insert and its partial index from becoming the write bottleneck at 1.2k writes/second?
Keep soft-deleted accounts from blocking re-registration
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.
How a strong candidate works it
- 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.
- Say what a soft delete must do besides setting deleted_at: increment auth_version so existing tokens stop validating, leave resource.owner_user_id and resource_revision.actor_user_id intact, and accept that the address is retained — erasure is a different requirement answered by scrubbing the column, not by a DELETE that would break those references.
Expected result (20 min): PostgreSQL rejects only the second live row and admits unlimited deleted ones. MySQL behaves identically with UNIQUE (tenant_id, email, deleted_marker) where live = 0; with the polarity reversed, two live rows commit and nothing afterwards can tell them apart.
Check yourself
- Group live rows by (tenant_id, lower(email)) after a randomised insert/delete run — no group exceeds one.
- Inserting 'User@x.test' after 'user@x.test' is rejected; if it is accepted, the case handling is in application code rather than in the index.
- EXPLAIN a login lookup and confirm the partial index is chosen — it is only usable by queries that carry the deleted_at IS NULL predicate, so a query without it falls back to a scan.
Follow-ups
- 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?
- What changes if a user may hold membership in two tenants?
Track a rolling failure rate per destination for circuit decisions
The egress service delivers about 1,500 webhooks per second across roughly 40,000 destinations, each call bounded by a 10 second timeout. Maintain, per destination, the failure rate over the trailing 60 seconds so a caller can ask before dispatch whether the circuit should open. Attempts arrive as (destination_id, finished_at_ms, outcome). Requirement: amortised O(1) per attempt, with total memory bounded by the destination count rather than by traffic. Give the structure, its exact memory, and the rule that stops a destination with three attempts from opening a circuit.
How a strong candidate works it
- Name the exact-deque version and then reject it as the default. Holding timestamps and advancing a tail pointer past anything older than now minus 60 seconds is a correct two-pointer window at amortised O(1) per attempt, but its memory tracks in-window traffic, so one destination in a retry storm holds hundreds of thousands of entries while thousands of quiet destinations hold none.
- Use a ring of 60 one-second buckets per destination, each bucket a pair of counters for attempts and failures. On an attempt, advance the ring by the elapsed whole seconds, zeroing at most min(elapsed, 60) buckets, then increment the head. That is amortised O(1) with a fixed footprint per destination.
- State the footprint: 60 buckets times two 4-byte counters is 480 bytes of payload per destination, so 40,000 destinations is roughly 20 to 25 MB with per-entry overhead, bounded by the catalogue rather than by the rate. The cost is granularity, since the oldest bucket ages out in whole seconds, which is far tighter than the decision needs.
- Require a minimum sample before the circuit may open. A destination with three attempts and three failures reads as 100 percent and is not evidence; a floor of roughly 20 attempts in the window makes the ratio meaningful, and below that floor use a run of consecutive failures as the trigger instead.
- Expire idle destinations, or memory grows with every destination ever seen rather than with the live set. Hold the rings in a bounded LRU keyed on destination_id and treat a miss as no history, which is the correct default for an endpoint that has been silent for a minute.
- Keep the half-open probe out of the window arithmetic. After the circuit opens, one probe per interval decides whether to close it, and folding that single success into a window that still holds a 100 percent failure history would reopen the destination on one data point.
Expected result (20 min): A per-destination ring of 60 one-second (attempts, failures) buckets advanced lazily for amortised O(1) cost, roughly 480 bytes of counters per destination and about 20 to 25 MB for 40,000 of them, an LRU bound on live rings, and an open rule requiring both a minimum attempt count in the window and a rate above threshold.
Check yourself
- After 60 seconds of silence a destination's window holds zero attempts and cannot open a circuit.
- Memory measured with one hot destination and 39,999 idle ones is indistinguishable from the reverse, which the deque version would fail.
- Replaying the same attempt stream with the clock shifted by an hour yields the same final rate.
Follow-ups
- The fleet is 30 instances and each sees roughly a thirtieth of a destination's traffic. Where does the rate actually live, and what does a per-instance answer get wrong?
- A destination answers in 9.5 seconds and succeeds. It is not failing but it is consuming your per-destination concurrency. What signal should open the circuit here?
- How would you make the window survive a process restart, and is it worth the cost?
Choose what to break when replication lag reaches forty seconds
Reads are served from two replicas: 14k requests/second, about 85% absorbed by cache, so roughly 2.1k reads/second reach the database. Writes go to the primary at 1.2k/second. A tenant's backfill drives replication lag from under 100 ms to 40 seconds and it is still climbing. Sessions that have just written are pinned to the primary. Decide, endpoint class by endpoint class, whether to serve stale, fail, or route to the primary, and justify each choice with the load it adds to the primary. Then state what you would have built beforehand.
How a strong candidate works it
- Establish blast radius before cause, because mitigation and diagnosis have different deadlines. The decisive arithmetic is what happens if the database reads move to the primary: 2.1k reads/second on top of 1.2k writes/second roughly triples its operation count, on the node already absorbing the backfill that caused this. Reads and writes are not equal in cost, so treat that as an argument against a blanket move rather than as a capacity model - but it is enough to rule out routing everything to the primary.
- Classify endpoints by what staleness costs, not by how important they feel. Reads whose staleness is invisible - listings, search, counters - stay on the replica and return the watermark so the client can tell. Reads that immediately follow that same session's write keep their primary pin, which is a small bounded slice of traffic rather than the whole 2.1k/second. Reads that feed a decision with a side effect - authorisation, quota, the read half of a read-modify-write - must not be stale at all, because a 40-second-old permission row is the stale-permission failure wearing a different costume; those go to the primary or fail.
- Shed instead of queueing. If the must-be-fresh class alone exceeds the primary's headroom, refuse its lowest-value slice with 503 and a retry-after. A request queued behind a saturated primary holds a connection for a client that has already given up, and the retry storm that follows is what turns degradation into an outage. Bound the connection pool per role so the read fallback cannot consume the write path's connections - that bulkhead is the single decision that determines whether writes survive the next ten minutes.
- Attack the cause in parallel, since it is the one thing that can be stopped. The backfill is the load generator. A backfill that reads replication lag as its throttle signal and pauses above a threshold would have made this a non-event, with batch sizes small enough that each batch's write volume is a fraction of what a replica can apply per second. That is most of the answer to what should have existed beforehand.
- Name the mechanism you would prefer over session pinning. Capture the write position at commit and require the read path to be at or past it: compare the primary's pg_current_wal_lsn() at commit time against the replica's pg_last_wal_replay_lsn(), and fall back to the primary only for the specific request that is ahead of the replica. Session pinning is the cheap approximation and it over-pins - every read in the window goes to the primary whether or not it needed to, which is a share of the cost being paid right now.
Expected result (35 min): Staleness-invisible reads stay on the replica and return a watermark. Reads following the session's own write keep a bounded primary pin. Reads that gate a side effect go to the primary or fail, with the lowest-value slice shed as 503 plus retry-after if headroom runs out. Connection pools are bounded per role so the read fallback cannot starve writes. The backfill is throttled on measured replication lag. The durable fix is a commit-position token compared against the replica's replay position, replacing blanket session pinning.
Check yourself
- The traffic routed to the primary under the plan is stated as a rate, not as a category.
- No element of the plan increases the write path's connection usage.
- The backfill throttle references a measured lag value and has an explicit resume condition.
- Every endpoint left on a replica returns something with which a client can detect staleness.
Follow-ups
- Lag returns to normal in nine minutes. Which mitigation do you remove first, and which one stays permanently?
- A user reports their change did not save, and the write committed. Trace the path that produces that report and name the signal that would have shown it before the report arrived.
- The replica is 40 seconds behind but otherwise healthy. Do you take it out of rotation? What does that do to the other replica's lag?
One log partition stops advancing while the others drain
Search results for a subset of tenants are hours stale; the rest are current. The projection consumer reports lag of zero on 15 of 16 partitions and 400,000 on one. Its error rate is flat and its CPU is idle. outbox_event has no pending rows older than a second, so the relay has published everything it holds. Identify the mechanism, give the ordered checks, and state what you do in the first ten minutes versus what you change permanently.
How a strong candidate works it
- Read the lag distribution first. A slow consumer lags everywhere; zero on fifteen partitions and 400,000 on one is not throughput. Idle CPU on the stuck partition means the consumer is not advancing its offset at all, which points at one message it cannot get past rather than at a rate problem.
- Exonerate the producer before touching the consumer. No pending outbox rows older than a second means the relay published, so the event exists in the log. This separates never sent from sent and never applied, which are different code paths and usually different owners.
- Read the message at the stuck offset and the handler's log lines for its event_id. A flat error rate with no progress has two explanations and you must distinguish them: the handler is throwing and the retry loop is swallowing it, or the handler is blocking on something and never returning. Idle CPU with no error lines favours the second.
- Mitigate before diagnosing further. Move the offending event to a dead-letter store and commit the offset past it. Adding consumers does nothing here, because a partition is consumed by exactly one member of the group, and the blast radius is every aggregate hashed to that partition, not only the aggregate that produced the bad event.
- Fix permanently by bounding handler attempts and dead-lettering on exhaustion, so no single message can stop a partition. Then replay the dead-lettered event once the handler is fixed: it carries aggregate_id and aggregate_version, so a consumer that discards versions it has already applied can absorb the replay, and resource_revision is the fallback if the event itself is unusable.
Follow-ups
- The dead-lettered event carried aggregate_version 7 and the projection had applied 6. What must the replay do differently if 8 and 9 landed in the meantime?
- How do you show staleness to the user while the partition is behind, given the API already returns the projection's watermark?
- What changes if the message is poison because a previous deploy wrote a payload shape the current code cannot parse?
Tell callers you do not own that their integration breaks
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.
How a strong candidate works it
- 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.
- State the removal criterion as a measurement with a duration attached, such as observed reads at zero across a full billing cycle, and keep the change reversible for one release after removal.
Follow-ups
- 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?
- After removal, what makes the change irreversible, and how long before you cross that line?
Ship under a deadline and bound the debt you chose
You have four days to ship a tenant-facing listing endpoint. The version you would defend uses keyset pagination over (tenant_id, status, updated_at DESC, resource_id DESC); the version you can finish uses LIMIT/OFFSET with no matching index. Describe a deadline call you actually made of this shape: what you shipped, what you knowingly deferred, how you bounded the damage with a mechanism rather than an intention, and the specific numeric condition that would force the follow-up. Name who you told and where you wrote it down.
How a strong candidate works it
- Name the deferred failure precisely instead of calling it slow. OFFSET n makes the database produce and discard n rows, so cost grows with page depth; without an index matching the sort, every matching row is read and sorted before the limit applies; and rows inserted between two page fetches shift across the boundary so items are skipped or repeated with nothing in the response to signal it.
- Bound the blast radius with something mechanical rather than a promise: cap maximum page depth, cap page size, restrict the endpoint to one internal caller, or keep it behind a flag. State which failure each cap removes and which it leaves standing.
- Attach a number to the trigger and wire it to an alarm: the first tenant crossing N resources, or the endpoint's p99 crossing its share of the 400 ms budget, so the debt announces itself instead of waiting to be remembered.
- Write it where the next engineer looks, which is the code and the ticket, not a chat message: what was deferred, why, the cap, and the trigger.
- Report what actually happened in your real example, including the case where the trigger never fired and the debt was correctly never repaid.
Follow-ups
- At what page depth does the offset version breach your latency budget, given your page size and row counts?
- What breaks first when you switch to keyset pagination later, and what does a client holding an old page token see?
- Who would have overruled you if you had asked for two more days, and did you ask?
Where candidates lose points
Designing a transfer or ledger flow where a retried request or redelivered message moves money twice
Reported design and API questions ask for exactly-once processing across unreliable networks and for concurrent deposits and withdrawals. State early that the network delivers at least once, then show how duplicates are made harmless: a client-supplied idempotency key stored with a unique constraint, a ledger entry keyed by transfer id so a second insert fails, and a consumer that records processed event ids or aggregate versions. Say what the caller sees when a retry hits an in-flight or completed request.
Fixing a concurrency bug in code review by wrapping everything in one lock without naming the invariant
For the reported code-review refactor question and the thread-safe exchange structure, first name the invariant being broken, for example that a balance never goes negative or that two withdrawals must not read the same starting balance. Then point to the exact check-then-act or read-modify-write that breaks it. Choose the narrowest fix (an atomic conditional update, a per-account lock, or a version check) and say what it costs in throughput and deadlock risk. One global lock fixes the race but adds a bottleneck and leaves the reasoning unexplained.
Over-building the first version of an evolving coding problem and never getting a working base solution
The source describes coding problems that grow in complexity. Get the base case running and tested first, for example a plain cache with get/put. Then add one constraint at a time: TTL expiry, then a memory cap. Keep each rule, such as the eviction policy or the expiry check, in one function so the next constraint is a small edit. A finished base version plus one clean extension is a better result than an extensible design with nothing that runs.
Treating the technical screen and onsite coding as code-only and being unprepared when a behavioral question arrives mid-session
Candidates report behavioral questions inside coding and design sessions as well as in separate conversations. Have short STAR answers ready for the source's prompts: a decision made with incomplete information, tough code review feedback, shipping speed versus paying down tech debt, disagreement between a PM and a tech lead, and a production failure you owned. Keep each answer brief so you can return to the technical problem without losing momentum.
Describing a past project in the deep dive without explaining why it was built that way
The onsite reportedly includes a deep dive into past technical work with hiring managers, and the question bank includes whiteboarding a past project. Before the interview, draw the system from memory. Mark the component you owned, the failure or scaling limit you hit, the alternatives you rejected with the reason for each, and what you would change now. Prepare for follow-ups on numbers and on decisions made by someone else.
Breadth then depth, for a generalist facing a mixed loop
Four days sample coding, design, fundamentals and the practical rounds at deliberately shallow depth, which is enough to surface the topics you did not know were in scope. That map, rather than a guess made on day one, decides where the last three days go.
Day 1 — Map the loop and prepare your background
- Write down the three reported stages (recruiter screen, technical assessment, virtual onsite) and the questions you will ask the recruiter: assessment format, accepted language, take-home or not, and which domain area the team supports
- Write two-sentence versions of your top three projects with one measured result each, and say them aloud to someone outside engineering
- Choose your interview language from those listed (Java, Go, Python, C++, TypeScript) and list the ten standard-library calls you look up most, then practise them until you no longer need to
Deliverable: A recruiter question list, three paraphrase-proof project summaries and a language cheat list you no longer need to open.
Day 2 — Coding: arrays, hash maps and sliding windows
- Solve the reported transaction sub-array threshold problem and state each method's limit: a running-sum sliding window handles contiguous sums only when amounts are non-negative; a prefix-sum hash map finds exact-sum sub-arrays (negatives allowed), not threshold ones; listing every qualifying sub-array is O(n^2) output in the worst case
- Work through bank-style problems: O(1) insert/delete/random set, isomorphic strings, bijective substitution cipher and weighted random pick
- For every problem, list the corner cases before coding (empty input, duplicates, negative amounts, equal values) and test them after
Deliverable: Five solved problems, each with its complexity and a written list of the corner cases tested.
Day 3 — Coding: custom structures, graphs and evolving problems
- Build an LRU cache from scratch, then add TTL expiry, then a memory cap, treating each step as a new constraint arriving mid-interview
- Implement BFS and DFS over a transfer or dependency graph for reachability, and use BFS for the fewest hops between two nodes, keeping a visited set so cycles do not loop forever
- Find the cheapest currency conversion path with Bellman-Ford on -log(rate) edge weights (Dijkstra only when no weight is negative), and treat a negative cycle as an arbitrage loop to detect and report
- Work the rolling failure-rate worked exercise (drill-coding-3) and compare your memory estimate with its expected result
- Write a line-by-line log parser that skips malformed records and aggregates counts per key
Deliverable: A cache that went through three constraint stages, a BFS/DFS reachability solution, a Bellman-Ford conversion path with cycle detection and one completed worked exercise.
Day 4 — API, concurrency and code review
- Design a deposit/withdraw ledger endpoint: request shape, idempotency key, response codes, and how two concurrent withdrawals on one account are serialised
- Write the optimistic-concurrency update from the drill-sql-1 question and compare it with the soft-delete uniqueness worked exercise (drill-sql-2)
- Take a small multi-threaded snippet with shared state, find the race, fix it with the narrowest change and explain the cost of the fix
- Sketch a token-bucket rate limiter and say how it behaves across several nodes
Deliverable: One endpoint spec with idempotency and concurrency rules, one reviewed-and-fixed concurrent snippet and one rate-limiter sketch.
Day 5 — System design: money movement
- Design the reported international transfer platform: requirements, consistency boundaries, the ledger write path, and what happens when a payout partner times out
- Design an event-driven ledger with exactly-once processing: outbox, at-least-once delivery, and deduplication at the consumer
- Run a shorter pass on real-time fraud scoring or FX rate calculation, stating what can be stale and what cannot
- Work the replication-lag worked exercise (drill-design-4) and note which of its decisions apply to a transfer system
Deliverable: Two full designs and one shorter one, each with a written list of failure modes and recovery paths.
Day 6 — Deep dive, product partnership and behavioral stories
- Whiteboard one past project from memory: components, what you owned, the failure you handled and two alternatives you rejected
- Prepare STAR stories for the source's prompts: decision with incomplete information, tough code review feedback, speed versus tech debt, PM and tech lead disagreement, and a production incident with its post-mortem
- Prepare a story about project slippage and how you communicated it, and one about pushing back on a product requirement
- Practise giving one story immediately after solving a coding problem, without a break
Deliverable: A project diagram you can draw from memory and seven short STAR stories.
Day 7 — Mock virtual onsite
- Run coding, design, a past-project deep dive and a behavioral session back to back, with at least one behavioral question inside a technical session
- Work the stuck-partition debugging drill (drill-debugging-5) as a production-diagnosis warm-up and say your checks out loud in order
- After the mock, note which session suffered from the one before it and where your opening habits (restating the problem, asking for constraints) disappeared
- Reduce the week to a one-page card of rules you can recite from memory
Deliverable: Mock notes on how one session affected the next, plus a one-page card of rules you can recite.