Plaid acts as an intermediary layer connecting consumers, bank accounts and fintech applications such as Venmo, Robinhood and Betterment. According to the role description, Software Engineers create, scale and maintain the APIs, real-time data pipelines and developer-facing tools behind that connection.
Much of the engineering problem is turning fragmented bank protocols, legacy financial backends and inconsistent data formats into standardized developer APIs. The role description mentions infrastructure teams working on high-throughput transaction processing, integrations teams building web scrapers and API connectors, and product teams working on identity verification and fraud prevention. Bank interfaces change without notice, so integration work includes automated monitoring and services that detect and isolate third-party failures.
Day-to-day work described for the role includes production code primarily in TypeScript, Go or Python, end-to-end ownership of features from spec to deployment and monitoring, code reviews, architectural decision records, on-call rotations, and work with product managers, data scientists and designers.
For interview preparation, that comes down to three things: practical coding on messy data with specs that change partway through, system design where external dependencies rate-limit, fail and change format, and stories that show ownership of production systems. This guide's rounds, questions and 7-day plan are built around those three.
Recruiter Phone Screen
reportedCandidates describe this as an initial call to align on your background, role expectations and compensation. It is also your best chance to learn the format of what comes next: whether the technical screen runs in your own local IDE or a hosted editor, which languages are accepted, and whether the final round includes a project presentation. Those answers decide how you spend your prep week, so ask for them directly.
What to demonstrate
- Whether your background maps to the role you applied for, described as systems and scope you owned rather than as titles
- Whether role expectations and compensation line up early, since compensation is one of the reported topics of this call
How to prepare
- Write a short summary of the systems you have built that touch APIs, data pipelines or third-party integrations, since those match the work described for this role
- Ask whether technical rounds use your own IDE or a hosted editor, which languages are accepted, and whether you will present a past project
- Settle a compensation range beforehand with base, bonus and equity named separately, so the question gets a number rather than a deflection
Technical Phone Screen
reportedA technical interview on coding and problem-solving. Candidate reports say technical screens are often run with you sharing your screen and coding in your own local IDE, with language documentation and a test setup allowed, and that there may be one or two screens. Reported practical coding questions, not tied to a specific round, include a template substitution engine, a text buffer with undo and redo, and a stateful rate limiter, so they are reasonable practice material for a screen. Treat whatever you get like writing a small pull request. Clarify the spec, build the smallest correct version, test it, then extend it.
What to demonstrate
- Whether you clarify the spec and edge cases (empty input, missing keys, malformed data) before writing code
- Whether the code is modular, with names and helper functions that make the next part of the problem cheap to add
- Whether you verify behavior with tests or printed checks instead of asserting that it works
- Whether you keep explaining your reasoning while you type rather than going silent
How to prepare
- Set up a scratch project in your chosen language with a test runner (Jest, PyTest or equivalent) and run one passing and one failing test, so no setup problem surfaces on the call
- Practise the reported template substitution question in separate steps: plain variable replacement, then nested references with cycle detection, then an explicit policy for missing keys
- Record one practice session with narration and check it for silent stretches and for code written before the spec was restated
Onsite Interview
reportedCandidates describe this as a series of interviews testing technical depth, architectural vision and behavioral alignment. Reports say the final round is made up of practical coding, system design, a project presentation and managerial or cultural discussions. The order is not stated, so prepare each as its own mode: finishing working code, reasoning about third-party failures at scale, defending your own past architecture decisions, and giving behavioral stories someone could check.
What to demonstrate
- Whether you switch cleanly between modes and do not over-build a coding problem or under-scope a design problem
- Whether your project presentation separates your own decisions from the team's and names the trade-offs you would revisit
- Whether facts about your work (team size, timeline, your role) stay consistent across different interviewers
How to prepare
- Build a project presentation around one architecture diagram, your personal contributions, the key trade-offs, the scaling problem you hit, and what you would design differently; reports say slides or a whiteboard may be requested
- Run one coding, one design and one behavioral mock back to back and ask each mock interviewer which answer sounded like the previous round
- Confirm with your recruiter whether final-round coding also happens in your own IDE, and keep that environment ready either way
Practical Coding Rounds
reportedInterviews on practical coding tasks and real-world engineering problems. Candidate reports describe multi-part prompts where the second and third parts build directly on the abstractions you wrote for the first, so hardcoded logic in part one turns into a rewrite later. Reported topics include object-oriented structure, parsing nested JSON or HTML payloads, string substitution, defensive handling of invalid input, and in-memory components such as caches with eviction and expiry. Aim for clean, modular code that is not over-engineered, and test as you go.
What to demonstrate
- Whether abstractions from the first part absorb later requirements without a rewrite
- Whether messy input is handled deliberately: nested payloads, malformed records, missing fields, null values
- Whether the data structure follows the access pattern, such as a hash map plus doubly linked list for O(1) LRU eviction, or undo and redo stacks for an editor
- Whether edge cases are covered by tests you actually run
How to prepare
- Build the text editor question in three passes: insert and delete at a cursor, then undo, then redo; record what each operation needs to store to be reversible and clear the redo stack on any new edit
- Implement an in-memory cache with LRU eviction and per-entry expiry, then add an LFU policy behind the same interface to check that your first design extends
- Write a parser that turns nested transaction payloads into normalized records, rejecting malformed ones with a reason and deduplicating repeats
System Design Interview
reportedCandidates describe a round on architecting systems for significant throughput and compliance. Reported system design questions include a distributed API gateway handling routing, rate limiting, authentication and logging; asynchronous webhook delivery with at-least-once guarantees to developer endpoints; a transaction ingestion service for real-time bank feeds; a link-generation service for authentication tokens; and a resilience layer for rate limits, downtime and structural changes from external banking partners. The common thread is dependencies you do not control, so name those failure modes before you draw components.
What to demonstrate
- Whether you pin down callers, scale, read-to-write mix and consistency needs before proposing an architecture
- Whether external bank dependencies are treated as unreliable, with circuit breakers, exponential backoff with jitter, rate limiting and fallback queues
- Whether delivery guarantees are stated and paid for: at-least-once delivery requires event ids and idempotent consumers
- Whether storage choices follow the data, relational for transactional consistency versus NoSQL for high-throughput unstructured data
How to prepare
- Design the webhook system end to end: durable queue, per-endpoint retries with backoff and jitter, a dead-letter path, and an event id consumers can deduplicate on
- Work the resilience-layer question with one failure at a time (rate limit, outage, changed response format) and state detection, containment and recovery for each
- Do the idempotency-key worked exercise for payment creation and practise explaining the concurrent-retry race in two sentences
- For the ingestion question, state the partition key, the per-account ordering guarantee and how a feed is replayed after a crash
Behavioral Conversations
reportedCandidates describe deep-dive discussions on collaboration and communication. Reported behavioral and project deep-dive questions, not tied to a specific round, cover walking through the architecture of something you built, a production outage you diagnosed and prevented from recurring, a high-impact project under a tight deadline or ambiguous requirements, prioritizing technical debt against features, and disagreeing with a product decision or a teammate's design. Other reported topics include working with product managers, handling ambiguity and learning from failure. Answer with specific decisions, the evidence behind them, and a clear line around your own part.
What to demonstrate
- Whether your stories separate the decisions you made from the ones the team or others made
- Whether an outage story covers diagnosis, resolution and the change that prevented a repeat
- Whether a technical-debt answer names a concrete trade-off and how you got agreement on it
- Whether you describe a mistake or failure plainly and say what you changed afterwards
How to prepare
- Map one story to each reported prompt and write the decision, the evidence and the outcome for each in three lines
- Rewrite each story replacing every plural pronoun with I or a named role, and cut any part you cannot speak to in first person
- Prepare an external-dependency failure story, since third-party outages recur across this guide's design and behavioral prompts
13 candidate reports. Individual accounts describe a particular role and hiring cycle.
Plaid Software Engineer Interview Experience — Two-Hour Screen Ends on a Syntax Error
I saw online that the first round is usually one hour, but I applied for a backend role and they required two hours back to back, with a 15-30 minute break in the middle. The first hour's question was a common pipeline question from the forum, and it went smoothly. After I finished, the interviewer was very insistent about how to test it, including wanting asserts in Python rather than print stat…
Read full experiencePlaid Customer Success Engineer take-home with client-style scenarios
My process started with a take-home technical assessment. It wasn’t a complex coding task. Instead, it involved real-life, client-style scenarios where I had to respond to technical issues the way a customer support engineer would. After I submitted it, there was essentially no communication. I received no feedback, had no recruiter contact, and had no real interaction during the process. The onl…
Read full experiencePlaid Software Engineer interview focused on algorithmic design
My interviews focused mostly on general problem solving, with algorithmic design questions instead of classic LeetCode-style problems. The bar itself wasn't clearly communicated, and the process didn't feel very professional. I also dealt with untimely responses and a sense that nothing was being evaluated consistently as the interviews progressed. By the time the process ended, it felt more like…
Read full experiencePlaid Customer Success Engineer interview with Quickstart homework and technical questions
I went through two stages. The first consisted of take-home exercises. I had to run Plaid’s Quickstart setup and write response emails to mock customers dealing with specific problems. I liked this part because it made me research what was happening and think through how to communicate in a calm, supportive way. The second stage was a one-on-one call. I was told it would be a short conversation w…
Read full experiencePlaid Software Engineer interview: CodeSignal OA and two technical rounds
My process started with an OA on CodeSignal, which was straightforward enough to complete. After that, I had a technical interview and a final round with a very specific rhythm: two technical interviews back to back, followed by one behavioral segment. The whole experience felt positive. The interviewers were genuinely friendly and engaged with my answers. They seemed interested in understanding…
Read full experiencePracHub editorial advice for the preparation topics above.
Losing the start of a local-IDE technical screen to runtime, path or test-runner errors
Candidate reports say technical screens are often run in your own IDE with screen sharing. The day before, open the exact editor, runtime and test runner you will use, create a fresh project, and run one passing and one failing test. Check that screen sharing shows your editor legibly, and keep a way to send your code afterwards (email or a Git repository), since reports say the final code may be requested.
Hardcoding part one of a multi-part practical coding prompt so parts two and three need a rewrite
Reported prompts such as the template engine, text editor and rate limiter grow in stages. Keep part one small but put the changeable decision behind a function or class boundary: the lookup for a template variable, the operation record for undo, the window policy for a rate limiter. Before extending, say out loud which piece the new requirement touches. If the answer is everything, fix the boundary first.
Designing a bank-integration system as if the external partner always answers correctly and on time
The reported design prompts (resilience layer, transaction ingestion, webhook delivery, API gateway) all hinge on dependencies that rate-limit, go down or change format. For each external call, state the timeout, the retry policy with backoff and jitter, the circuit-breaker condition, and what the caller sees while the partner is down. For at-least-once webhooks, say how consumers deduplicate, because retries without an event id create duplicates.
Going silent while coding, so the interviewer cannot follow your choices
Candidate reports describe these rounds as collaborative. Before typing, restate the input contract and your approach; while typing, name each edge case as you handle it; after each part, run a test and say what it proves. If you get stuck, state the hypothesis you are checking instead of editing code at random.
Presenting a past project where your own decisions blur into the team's
For the project presentation and the behavioral conversations, mark which components you designed, which you implemented, and which you inherited. Prepare the trade-off you would reverse today and the scaling problem you actually hit, with the numbers you can back up. Walking through an architecture without naming your part leaves nothing to assess, however clean the diagram.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Design an in-memory text editor or buffer data structure supporting co…
Design an in-memory text editor or buffer data structure supporting core operations like insert, delete, move cursor, undo, and redo.
Approach
- Name the brute-force solution and its complexity before improving on it.
- Restate the input: its shape, its size, and what is guaranteed about it.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- Which test case would catch an off-by-one here?
- What is the worst case, and how likely is it on real data?
Implement a stateful rate limiter that tracks API request rates across…
Implement a stateful rate limiter that tracks API request rates across varying customer tiers and time windows.
Approach
- Restate the input: its shape, its size, and what is guaranteed about it.
- State the target complexity and say which constraint rules the naive version out.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- Which test case would catch an off-by-one here?
- How does this change if the input no longer fits in memory?
Implement a template substitution engine that takes a text template wi…
Implement a template substitution engine that takes a text template with variables and dynamically replaces them using a provided key-value dictionary, handling nested references and edge cases.
Approach
- Name the brute-force solution and its complexity before improving on it.
- State the target complexity and say which constraint rules the naive version out.
- Walk one small example through your approach before writing the whole thing.
Follow-up
- What is the worst case, and how likely is it on real data?
- How does this change if the input no longer fits in memory?
Write a function to validate and normalize dirty account number inputs…
Write a function to validate and normalize dirty account number inputs against varying international banking standards.
Approach
- Walk one small example through your approach before writing the whole thing.
- Name the brute-force solution and its complexity before improving on it.
- Choose the data structure from the access pattern, not from familiarity.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
Derive per-account balances and catch unbalanced transactions
You are given ledger_entry rows streamed in entry_id order: transaction_id, account_id, direction (debit or credit), amount_minor (a positive int64), currency, business_date. Up to 500 million rows, at most 20 million distinct (account_id, currency) pairs, and the entries of one transaction are contiguous in the stream. In a single pass with no re-reads, return the closing balance per (account_id, currency) and the transaction_id of every transaction whose entries do not sum to zero within each currency. State your time and space bounds.
Approach
- Normalise the sign at read time from
direction, not from the amount:signed = +amount_minorfor debit,-amount_minorfor credit (state which convention you picked). The schema constrainsamount_minor > 0precisely so the sign lives in exactly one place. - Hold one hash map keyed
(account_id, currency)to an int64 running total. Twenty million keys at 16 bytes of payload plus map overhead is order 1 GB in most runtimes — quote the number, and offer the fallback: partition the stream byhash(account_id) % Pand run P passes for 1/P of the memory. - Ride the zero-sum check on the same pass. Because a transaction's entries are contiguous, keep a tiny
currency -> int64map for the currenttransaction_idonly, test it against zero on the boundary and at EOF, then clear it. That is O(currencies in one transaction), typically one or two. - Bound the arithmetic explicitly. Int64 holds about 9.22e18, so overflowing one account across 500 million entries needs an average of 1.8e10 minor units per entry — safe here, but use a checked add so an adversarial file fails loudly rather than wrapping.
- Complexity: O(n) time, O(distinct account-currency pairs) space, one sequential pass, no sort. The zero-sum check adds no asymptotic cost, which is the argument for doing it here rather than in a second job.
Worked solution 20 min
- Write the sign rule down in one sentence before any code, naming which side debit is positive on, and apply it at read.
- Implement with two maps —
balances: (account_id, currency) -> int64andtxn: currency -> int64— plus the currenttransaction_id. - On a change of
transaction_id, assert every currency intxnsums to zero, record the id if not, then clear. - Feed a fixture: one 2-entry transaction that balances; one 4-entry transaction with USD and JPY legs that balances within each currency; one 3-entry transaction off by a single minor unit.
- Re-run with the entries shuffled inside each transaction to prove the result is order-independent within a transaction.
Follow-up
- Entries of a transaction are no longer contiguous. What does the zero-sum check cost now, and which is cheaper: buffering open transactions or an external sort on
transaction_id? - How would you produce the same balances as of an arbitrary
business_datewithout a second full scan? - The job is restarted after a crash halfway through the file. What makes the second run produce identical output?
Store multi-currency amounts and prove each transaction balances
ledger_entry holds entry_id, transaction_id, account_id, direction (debit or credit), amount_minor (bigint, CHECK > 0), currency char(3), source_type, source_id, business_date and posted_at. The service posts in JPY, USD and KWD. Specify the column types for money and the reference table that carries each currency's ISO 4217 minor-unit exponent, and say why that exponent must not be a constant in application code. Then write the query that returns every transaction_id whose entries fail to sum to zero within each currency, ordered by the largest absolute imbalance.
Approach
- State the representation: amount_minor is an integer count of minor units and the scale lives with the currency, not in the column. int4 tops out at 2,147,483,647 minor units, which is about 21.5 million units of a 2-exponent currency, so bigint, and the currency code travels on every row that carries an amount.
- Normalise the scale into currency(code char(3) primary key, exponent smallint, name) and join it only at display and parse boundaries. The exponent is 0 for JPY and KRW, 2 for USD and EUR, 3 for KWD, BHD, JOD, OMR and TND, so a hard-coded divide by 100 is wrong by 100x for JPY and by 10x for KWD, in opposite directions.
- Rule out binary floating point outright: IEEE 754 binary64 cannot represent 0.1, so repeated accrual drifts by a few minor units that later appear as reconciliation breaks. If a fixed-scale decimal is used instead, pin the scale per currency and name the single place rounding happens.
- Write the balance check as a grouped aggregate: sign the amount with a CASE on direction, GROUP BY transaction_id, currency, and HAVING the signed sum <> 0. The sign belongs in direction, and the CASE is the one place it becomes arithmetic.
- Then order in a second layer, because the ordering the question asks for cannot be written on the grouped query itself. PostgreSQL lets an output alias stand alone in ORDER BY but resolves anything inside an expression against the input columns, so ORDER BY abs(imbalance_minor) DESC raises SQLSTATE 42703, column "imbalance_minor" does not exist, while the bare ORDER BY imbalance_minor DESC is accepted and sorts signed rather than absolute. Wrap the grouped query and order by abs() outside it, or repeat the aggregate as ORDER BY abs(SUM(CASE ...)) DESC, which is legal because aggregates are allowed there.
- Group per (transaction_id, currency) rather than per transaction, because a cross-currency movement balances only within each currency, bridged by an explicit FX position account whose rate, source and timestamp are stored on the transaction.
- Note that no CHECK constraint can express this, since it spans rows: enforce it at write time with a DEFERRABLE INITIALLY DEFERRED constraint trigger that fires at commit, or by routing every posting through one function, and keep this query as the independent audit.
Worked solution 15 min
- Define currency(code, exponent, name) and seed JPY 0, USD 2, KWD 3; declare amount_minor bigint NOT NULL CHECK (amount_minor > 0) and currency char(3) REFERENCES currency(code).
- Write it in two layers so it runs: SELECT transaction_id, currency, imbalance_minor FROM (SELECT transaction_id, currency, SUM(CASE WHEN direction = 'credit' THEN amount_minor ELSE -amount_minor END) AS imbalance_minor FROM ledger_entry GROUP BY transaction_id, currency HAVING SUM(CASE WHEN direction = 'credit' THEN amount_minor ELSE -amount_minor END) <> 0) b ORDER BY abs(imbalance_minor) DESC.
- Post one clean capture, one deliberately unbalanced transaction, and one cross-currency transfer with an FX position account, then run the query.
- Convert 1000 JPY and 1000 KWD through the exponent table in both directions and confirm the round trip is exact.
Follow-up
- A transaction has a USD leg and a JPY leg. Which accounts appear, and where does the rounding residual land?
- How would you enforce the zero-sum rule at write time without serialising all postings behind one lock?
- Someone proposes storing a display amount alongside amount_minor. What goes wrong at the first rate change or rounding rule change?
Stop the capture-and-refund join from double-counting merchant money
Report captured_minor, refunded_minor and net_minor per merchant per business_date. payment_intent holds intent_id, merchant_id, amount_minor, captured_minor, refunded_minor. ledger_entry holds transaction_id, account_id, direction, amount_minor, source_type in (capture, refund, fee, chargeback), source_id = the intent_id as text, and business_date; every capture posts one debit and one credit. A draft joins payment_intent to ledger_entry twice, once filtered to captures and once to refunds, and sums both. On intents with two partial captures and one refund the totals are wrong. Name both multiplications precisely and write the correct query.
Approach
- Name the first multiplication by cardinality: intent to capture entries is one-to-N and intent to refund entries is one-to-M, so joining both yields N times M rows per intent. Every capture row is then counted M times and every refund row N times, and the two columns are inflated by different factors, which is why the totals look almost right rather than obviously broken.
- Name the second, which is easier to miss: double-entry means each capture posts two legs, so summing every entry of a capture counts the amount twice regardless of joins. Select the single leg by ACCOUNT - the merchant's settlement payable, which carries exactly one leg of every capture and of every refund - and not by direction. Direction is not a substitute for that filter: a capture credits the payable and a refund debits it, so adding direction = 'credit' keeps the captures and silently drops every refund leg, leaving refunded_minor at exactly zero on a query that otherwise reads as correct.
- Reject the reflexive fixes: SELECT DISTINCT and SUM(DISTINCT ...) de-duplicate values, not rows, so two genuine captures of equal amount collapse into one and the total is wrong in the other direction while looking tidier.
- Prefer a single pass with conditional aggregation over ledger_entry, restricted to that one account: SUM(amount_minor) FILTER (WHERE source_type = 'capture') and the same for refunds, grouped by merchant and business_date, joining payment_intent only to reach merchant_id. One scan, no intent-level fan-out, and fees or chargebacks are added as another FILTER rather than another join. amount_minor is a positive magnitude, so net_minor is captured minus refunded; the signed form over the same rows, SUM(CASE WHEN direction = 'credit' THEN amount_minor ELSE -amount_minor END), returns the same net in one column and is the check on the sign convention.
- Keep aggregate-then-join in reserve for when per-intent detail is required: aggregate each side to intent grain in its own CTE, then join the two aggregates one-to-one. LATERAL works too and reads better when the right side needs the left's key.
- Get the account predicate, the join column and the date right. Each merchant has its own settlement payable, so the predicate is a join to the account dimension (a.account_type = 'merchant_payable' AND a.merchant_id = pi.merchant_id); a single $settlement_account literal is only correct for a one-merchant report. source_id is text and intent_id is uuid, and PostgreSQL has no implicit cast between them, so one side must be cast and the side decides which index stays usable - pi.intent_id = e.source_id::uuid probes payment_intent's primary key per entry row, e.source_id = pi.intent_id::text probes an index on ledger_entry(source_id), and if neither fits the driving table the planner hashes both sides, which is fine for a day's report and not for a single-intent lookup. Group on ledger_entry.business_date rather than posted_at::date, because they differ across the cutoff, and decide explicitly whether a later-dated refund reduces its own day or the original capture's.
Follow-up
- A refund lands on a later business_date than its capture. Which day does net_minor move, and what does that do to a daily merchant payout?
- Add scheme fees, which arrive netted into a batch total rather than per transaction. How does the query change, and what can it no longer claim?
- How would you reconcile this output against payment_intent.captured_minor, and which one is authoritative when they disagree?
Design a resilience layer that gracefully handles rate limits, unexpec…
Design a resilience layer that gracefully handles rate limits, unexpected downtime, and structural changes from external banking partners.
Approach
- Choose a partition key and say what query it makes expensive.
- State the consistency you need, and where you are willing to be stale.
- Name the failure you are designing for, then the recovery path.
Follow-up
- What would you drop to keep the system up under load?
- What breaks first when traffic grows ten times?
Design a high-throughput transaction ingestion service that processes …
Design a high-throughput transaction ingestion service that processes real-time bank feeds and writes to scalable persistent storage.
Approach
- Choose a partition key and say what query it makes expensive.
- Fix the scope first: who calls this, how often, and what they do when it fails.
- State the consistency you need, and where you are willing to be stale.
Follow-up
- What breaks first when traffic grows ten times?
- How does this behave when that dependency is down for an hour?
Specify the idempotency key contract for payment creation
POST /payments takes an idempotency key header plus amount_minor, currency, merchant and instrument token. The caller is a merchant server that retries on timeout and may retry concurrently from two workers. Storage is idempotency_key (id, scope text, key text with UNIQUE (scope, key), request_fingerprint bytea, status enum in_progress/completed/failed, response_status smallint, response_body jsonb, locked_at, completed_at, expires_at). Specify the scope, what the fingerprint covers, the response when the same key arrives with a different body, the response while the first request is still in flight, and what a replay after expires_at does.
Approach
- Scope the key to caller plus operation, for example 'merchant:1234:create_payment'. A global key space lets two merchants collide, and a key scoped only to the merchant lets a key used on one endpoint replay that endpoint's stored response for a different operation.
- Fingerprint the canonicalised body: sorted keys, no insignificant whitespace, amounts as integer minor units. Cover the fields that determine the effect and deliberately exclude client metadata such as a trace id, so a retry that adds one is still recognised as a retry. Same key with a different fingerprint is 409 with its own code, never the cached response and never the new effect.
- Win the concurrent race in the database: INSERT ... ON CONFLICT DO NOTHING RETURNING id. An empty return means this request lost, so it reads the winner's row. A SELECT-then-INSERT is not a fix; under READ COMMITTED both workers take fresh snapshots and both pass the check.
- Define in_progress explicitly. The loser reads status in_progress and receives 409 with a retry hint rather than a blocking wait or a second attempt. Note that Retry-After is defined for 429, 503 and 3xx, so on a 409 it is an extension you document rather than something the caller's HTTP stack already honours. A locked_at older than the lease is reclaimable only through a conditional UPDATE that matches the old locked_at, otherwise two reclaimers both proceed.
- Commit the key row before calling the processor, so a crash leaves evidence of an attempt that can be queried and converged. On completion store response_status and response_body and replay them verbatim, including the original status code.
- State the retention window (24 hours is a common choice) and its consequence: after expires_at the same key is a new request, so the caller's total retry deadline must sit inside the window or the mechanism silently stops protecting anything. The header name comes from an IETF draft rather than a ratified RFC, so document it as your convention.
Worked solution 30 min
- Implement the endpoint over PostgreSQL with UNIQUE (scope, key), ON CONFLICT DO NOTHING RETURNING, an in_progress row written and committed before the processor call, and a stored response replayed on completion.
- Fire 50 concurrent identical requests with one key and assert one payment row, one processor call, and 50 byte-identical response bodies and status codes.
- Re-send the same key with amount_minor changed and assert 409 with the fingerprint-mismatch code and no new payment row.
- Kill the process between the processor call and the local commit, restart, and let the sweeper query the processor by the stored reference; assert the key converges to completed with exactly one charge.
- Advance the clock past expires_at, replay the key, and assert the documented behaviour (a new payment) rather than an undefined one.
Follow-up
- The processor call succeeds but your commit fails. What does the next request with the same key see, and what converges the row?
- A merchant reuses one key for two genuinely different payments a month apart. Which of your rules catches it, and which lets it through?
- Where does the key live end to end, and what changes if the downstream processor honours its own key?
Authorisation p99 tripled overnight with no deploy
Payment orchestration serves about 3,000 authorisations per second against a 150 ms p99 budget. Since 02:00, p99 is 460 ms and rising about 8 ms per hour, while p50 is unchanged at 11 ms. There was no deploy, no traffic change and no processor degradation. The hot path does one INSERT into idempotency_key, which has UNIQUE (scope, key), then two UPDATEs on that row: locked_at, then status and response_body. A nightly reconciliation job started at 01:50 and is still running. Give the ordered diagnostic checklist and the cause.
Approach
- Read the shape first. p50 flat with p99 rising and no deploy is a resource or data-volume effect, not a code path, because a code change moves the median too. A tail that climbs monotonically at fixed workload means something monotonically grows.
- Ask what started at 01:50. In PostgreSQL an open transaction holds back the xmin horizon cluster-wide, so autovacuum can reclaim no dead tuple newer than that snapshot. Confirm with pg_stat_activity (state, now() - xact_start, backend_xmin) and with pg_stat_all_tables (n_dead_tup, last_autovacuum) for idempotency_key.
- Connect it to the write pattern. Three writes per key produce up to two dead tuples each, so at 3,000 rps the table sheds roughly 6,000 dead tuples per second. Heap-only tuple updates would keep those out of the index, but only when no indexed column changes and the page has room, and appending response_body grows the tuple enough to force a new page. So the unique index on (scope, key) grows too.
- Explain why only the tail suffers. A larger index means more pages per lookup and a rising fraction of them missing shared_buffers; the median request still hits cache while the tail pays physical I/O. This is exactly the p50-flat, p99-rising signature, and it is worth stating before acting.
- Verify before fixing rather than after. n_dead_tup in the millions and rising, last_autovacuum stale since about 01:50, and pg_relation_size on the unique index measured twice fifteen minutes apart showing growth at constant workload. Index bloat cannot be inferred from row count alone; use the size series or pgstattuple.
- Fix in two moves and prevent separately. End or chunk the long transaction so the reconciliation job commits per batch instead of holding one snapshot over 50M lines, then let autovacuum catch up or run REINDEX CONCURRENTLY. Add a transaction-age alert and a statement timeout on the reporting role. Collapsing the two UPDATEs into one and lowering fillfactor halves dead-tuple production, but that is an optimisation, not the cause.
Follow-up
- The reconciliation job legitimately needs a consistent view of 50M lines. How do you give it one without pinning the xmin horizon?
- Why did p50 not move at all?
- You also run an expires_at cleanup job that DELETEs old idempotency keys. During this incident, does running it help or hurt, and what design avoids the question entirely?
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Environment and recruiter screen
- Configure the editor, language runtime, debugger and test runner you will use in a local-IDE technical screen; create a fresh project and run one passing and one failing test
- Rehearse screen sharing with the editor at a readable font size, and set up a way to send code afterwards by email or a Git repository
- Write a short background summary focused on APIs, data pipelines or third-party integrations you have owned
- List the questions for the recruiter: IDE versus hosted editor, accepted languages, whether a project presentation is included, and the expected timeline
Deliverable: A working scratch project with a one-command test run, plus one page of screen notes with your background summary, compensation range and recruiter questions.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Multi-part practical coding
- Solve the template substitution question in three timed parts: plain substitution, nested references with cycle detection, then a missing-key policy; add tests after each part
- Build the text editor buffer in three passes: insert and delete at a cursor, undo, then redo; make sure a new edit clears the redo stack
- Narrate both sessions out loud and note every point where a later part forced you to change earlier code
Deliverable: Two solutions with passing tests, and a short note on which first-part boundary made each extension easy or hard.
Practice prompt ↗Practice prompt ↗03Stateful components and messy input
- Implement a per-tier rate limiter twice, once as a sliding window and once as a token bucket, and write down the memory and accuracy trade-off between them
- Build an in-memory cache with LRU eviction and per-entry expiry, then add LFU behind the same interface
- Write an account-number validator and normalizer that strips separators, rejects bad lengths and characters, and returns a reason for each rejection
- Parse a batch of nested transaction payloads into normalized records, deduplicating repeats in one pass
Deliverable: Four components, each with boundary tests (exact capacity, one past capacity, expired entry, malformed input).
Practice prompt ↗Practice prompt ↗04Algorithms on transaction data
- Compute rolling averages with a sliding window and flag anomalies against them, stating the window update cost
- Return the top spending categories over a sliding time window with a heap, and handle entries expiring from the window
- Detect circular transfer paths between accounts with DFS and a visiting set, and state the complexity
- Work the ledger balances coding exercise (derive per-account balances and catch unbalanced transactions) and check your result against its listed checks
Deliverable: Four solutions with stated time and space complexity, and the worked coding exercise passing every check.
Practice prompt ↗Practice prompt ↗Worked solution ↗05SQL and data modeling
- Work the multi-currency ledger SQL exercise end to end and run the query rather than reading it
- Solve the capture-and-refund double-counting question and name both sources of multiplication before writing the fix
- Write a one-page SQL versus NoSQL comparison for the transaction ingestion question: consistency, query flexibility, scaling and schema changes
Deliverable: Two executed SQL solutions with test data, and a storage-choice note you can say in under a minute in a design round.
Practice prompt ↗Practice prompt ↗06System design around unreliable partners
- Design the resilience layer for external banking partners, handling rate limits, outages and format changes one at a time with detection, containment and recovery
- Design asynchronous webhook delivery with at-least-once guarantees: queue, retries with backoff and jitter, dead-letter path, and event ids for consumer deduplication
- Sketch the transaction ingestion service with a partition key, per-account ordering and a replay path
- Work the idempotency-key design exercise for payment creation and explain the concurrent-retry race without notes
Deliverable: Three written designs, each opening with scope and failure modes, plus the idempotency exercise checked against its listed checks.
Practice prompt ↗Practice prompt ↗07Project presentation, behavioral stories and a full mock
- Build the project presentation: one architecture diagram, your own contributions, trade-offs, the hardest problem, and what you would change
- Prepare one story each for the reported behavioral prompts: architecture walkthrough, outage, deadline or ambiguity, technical debt versus features, and a design disagreement
- Talk through the latency debugging drill (p99 rising with p50 flat) out loud, stating each hypothesis before each check
- Run a mock sequence of practical coding in your local IDE, one design question and the presentation, and note where you slipped between modes
Deliverable: A presentation you can deliver with a diagram, five written stories with your own part marked, and notes from the mock sequence.
Practice prompt ↗Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
The reported behavioral prompts for this role are about ownership of real systems: outages, deadlines, technical debt and design disagreements. For each, prepare one story that separates your part from the team's, names the evidence you used, and says what you would do differently. The drill prompts in this guide's question list (arguing against a dual write, reversing a sharding decision, a review dispute over isolation level) are good practice for the deeper follow-ups.
Argue against a dual write you were assigned to build
A senior engineer specifies that the orchestration service should update payment_intent.status and publish the merchant event in the same code path, wrapping the publish in a retry. You believe it is wrong and you have been told to build it. Describe a time you argued against a design you were assigned. State the failure you predicted, the evidence you brought, how long the disagreement ran, what you did once the decision went against you, and what production eventually showed. Include the version of your argument that did not persuade anyone, and why it did not.
Approach
- The probe is whether you can disagree with a technical authority using evidence rather than taste, and still execute. Establish the failure precisely first: no transaction spans the database and the broker, so a crash between them leaves one side done, and it fails asymmetrically in both orderings. Publish-then-commit invents events for state that never existed; commit-then-publish loses events for state that does.
- Kill the retry argument explicitly, because it is the one that keeps the design alive: the retry loop lives in the same process that can die, so it narrows the window and never closes it. Quantify the window if you can (deploy restarts per week times request rate times the in-flight fraction) rather than asserting it is rare.
- Bring evidence in the form the decision-maker can check in a day: a count of merchant events with no corresponding intent version, or intent versions with no event, over a window you can query now. An argument that costs the other person nothing to verify is the one that moves.
- Say what you proposed instead in one sentence with its cost owned honestly: an outbox row written in the same transaction with UNIQUE (aggregate_type, aggregate_id, aggregate_version), a partial index on published_at IS NULL, and a relay that delivers at least once. The cost is that every consumer now has to be idempotent, and that is a real tax you are asking others to pay.
- Describe the disagree-and-commit mechanics concretely: what you built, what you instrumented so the prediction could be checked, and what threshold would have proved you wrong. A strong answer is falsifiable; a generic one says 'I raised concerns and moved on'.
- Report the outcome including the possibility that you were partly wrong about severity or timing, and separate 'I was right' from 'the disagreement was handled well'.
Follow-up
- You lost the argument. What instrumentation do you add so the question gets settled by data in a month rather than by another meeting?
- The stream processor's documentation says exactly-once. Why does that not settle the question for a card charge?
- What would have made you drop the objection entirely?
Reverse a sharding decision after production contradicted it
To raise throughput past a single row's lock ceiling, you shard a hot settlement account balance into 16 sub-rows. Two weeks later the floor check has to sum all 16 under a stronger isolation level, contention has moved rather than gone, and operations cannot explain the balance to an auditor. Describe a decision you reversed. State what you believed when you made it, the measurement that changed your mind, how you unwound it without causing a second incident, and how long you waited before concluding the data was real rather than noise.
Approach
- The probe is whether you can hold a belief loosely and unwind your own work without ego. Begin with the reasoning that was correct at the time: a single balance row commits at roughly one write per lock hold, so at a 4 ms hold you get about 250 writes per second regardless of core count, and sharding is the standard answer to that ceiling.
- Name what the original reasoning missed rather than calling it a mistake in general. The floor predicate was a single-row CHECK before sharding and became a cross-row predicate after it, so every write now either sums the shards under SERIALIZABLE with a bounded retry on 40001 or locks them in a fixed order to avoid 40P01. The throughput gain is real but smaller than 16x, and the auditability cost was never priced.
- Give the measurement that decided it, with a before and an after: committed writes per second, p99 write latency, retry rate on 40001, and the time an analyst needs to reconstruct one balance. A reversal justified by feel is the generic answer.
- Describe the unwind as a migration, not a revert: shadow the consolidated balance, reconcile it against the sum of shards over a full business day including the cutoff, cut reads over first, then writes, keeping the shards readable until one full reconciliation cycle has passed clean.
- State the waiting rule you used before acting. Two weeks of a moving p99 can be a deploy or a traffic shift; a strong answer names the signal that separated a trend from noise, such as the retry rate persisting across a low-traffic weekend.
- Close with what you would keep. Some of the work is usually salvageable (the instrumentation, the lock ordering, the measured ceiling), and saying which parts survived shows the reversal was analysed rather than abandoned.
Follow-up
- You still need the throughput. What is the next thing you try, and what does it cost the floor check?
- How do you reconcile the sharded balance against the consolidated one during the migration without double counting entries posted mid-cut?
- What would you have measured before the original change that would have made the answer obvious?
Resolve a review disagreement over isolation level
A colleague's pull request reads a balance, compares it to a floor in application code, then issues an UPDATE with the computed value, all under PostgreSQL's default READ COMMITTED. You comment; they reply that staging has never produced a negative balance. Describe a code review disagreement where you were confident and the author was not convinced. State how you made the failure concrete rather than theoretical, how many rounds it took, at what point you would have escalated or approved anyway, and what you conceded to the author.
Approach
- The probe is whether you can convert a correctness objection into something reproducible instead of a stalemate of opinions. Name the anomaly by its mechanism: under READ COMMITTED each statement takes a fresh snapshot, so two sessions can both read balance 100, both compute 100 minus 80, and both write 20.
- Address the staging evidence directly rather than dismissing it. Staging concurrency on one account row is effectively one, so the absence of the anomaly there is expected under both the broken and the correct implementation. That is the sentence that usually ends the argument.
- Reproduce it in two psql sessions and paste the interleaving into the review. A twelve-line transcript settles in one round what three paragraphs of theory will not settle in four.
- Offer the fix as a choice with its trade-off, not as a verdict: an atomic UPDATE ... SET balance = balance - $1 WHERE account_id = $2 AND balance - $1 >= $3 with a rowcount check keeps it single statement and needs no retry; SELECT ... FOR UPDATE serialises the row and lets you compute in application code; SERIALIZABLE covers the multi-row version of the predicate but requires a bounded retry on SQLSTATE 40001 that someone has to actually write.
- Say where your bar is. Correctness on money is a blocking comment, style is not, and a strong answer states that boundary before the disagreement rather than discovering it during one.
- Name what you conceded. The author was usually right about something (scope, naming, the follow-up being separable), and saying so is what makes the blocking comment land next time.
Follow-up
- The author switches the service to MySQL. Which of the three fixes still behaves the same, and which changes silently?
- The same endpoint later transfers between two accounts. What do you now require in the review?
- How do you keep this from being relitigated in every future pull request?
- 01
Walk through the architecture of a complex feature or service you built in a prior role, highlighting the key trade-offs and technical decisions.
- 02
Describe a major outage or production bug in your system. How did you diagnose it, resolve it, and prevent it from recurring?
- 03
Tell me about a high-impact engineering project you delivered under a tight deadline or with ambiguous requirements.
- 04
How do you prioritize technical debt against new product feature requests when planning engineering sprints?
- 05
Describe a time you disagreed with a product decision or a teammate's technical design. How did you resolve it?
- 06
Tell me about a time an external dependency failed unexpectedly. How did you manage the impact on users and the technical resolution?
Is this an official Plaid interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Plaid. Rounds and questions reflect what candidates have reported, not a process Plaid has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗Should I prepare for standard LeetCode style questions or practical coding?
Both, weighted toward practical coding. Candidate reports describe multi-step implementation questions such as a text editor with undo and redo, a template substitution engine or a stateful rate limiter, where clean structure and data structure choice matter. Algorithmic prompts are also reported, including sliding-window rolling averages, heap-based top categories over a time window, and graph traversal to detect circular transfers, so keep core patterns fresh as well.
PracHub interview research ↗What programming language should I use during the technical interviews?
Candidates report being able to choose a mainstream language such as TypeScript, Python, Go, Java or C++. Reports note that TypeScript is used widely in Plaid's stack and that TypeScript or Python make JSON handling in a local setup quick. Pick the language whose standard library, test runner and debugger you can use without looking things up, and confirm the options with your recruiter.
PracHub interview research ↗How does the local environment setup work for technical interviews?
Candidate reports say the interviewer typically shares a prompt and starter code over chat or email, you share your screen and write and run code in your own IDE, verify it with unit tests or printed checks, and send the final source afterwards by email or a Git repository. Have your editor, runtime and test runner working before the call.
PracHub interview research ↗How long does the interview loop take from start to finish?
Reports differ. The round summary cites roughly 4-6 weeks, while other candidate notes cite 3 to 5 weeks from first screen to decision, with feedback typically within 3 to 5 business days after each major round. The length depends on scheduling availability, so ask your recruiter for the expected timeline and mention any competing deadline early.
PracHub interview research ↗What should I prepare for the project presentation?
Reports say the final round includes a project presentation, and that you may be asked to present slides or whiteboard the architecture of a major system you built. Prepare one clear architecture diagram, the parts you personally designed and wrote, the key trade-offs, the hardest scaling or reliability problem and how you handled it, and what you would build differently now. Expect follow-up questions that test each trade-off.
PracHub Software Engineer practice ↗What system design topics come up for this role?
Reported design prompts include a distributed API gateway with routing, rate limiting, authentication and logging; asynchronous webhook delivery with at-least-once guarantees; a transaction ingestion service for real-time bank feeds; a link-generation service for authentication tokens; and a resilience layer for rate limits, downtime and format changes from external banking partners. Prepare circuit breakers, backoff with jitter, message queues, caching, idempotency keys and relational versus NoSQL storage choices.
PracHub Software Engineer practice ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
platform · Accessed 2026-09-24 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
platform · Accessed 2026-09-24 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
platform · Accessed 2026-09-24