cswork / libraryOpen practice workspace

Interview guides · Stripe

Stripe Software Engineer Interview Guide 2026

Prepare for Stripe SWE interviews with runnable pagination and retry mocks, a reproduced lost-response bug, 25 checks and a focused two-week work plan.

Interviewing at Stripe

  • Practice bank: 108+ questions
  • Rounds: 4
  • Typical prep: 2–4 weeks
  • Interview reports: 42

Interview rounds

RoundQuestions
HR Screen4
Online Assessment17
Technical Screen56
Onsite31

How hard is the Stripe Software Engineer interview?

DifficultyShareQuestions
Easy4%4
Medium81%88
Hard15%16

What Stripe actually tests for

TopicShareQuestions
Coding & Algorithms50%54
Software Engineering Fundamentals22%24
Behavioral & Leadership13%14
System Design10%11
Data Manipulation (SQL/Python)5%5

Prepare for a Stripe software engineer interview by practising complete engineering tasks: parse unfamiliar data, implement a small change, debug a failure and explain how an API behaves when a response disappears. Keep your data-structure fundamentals, but add runnable integration and debugging work to your preparation.

This guide's existing round coverage includes programming, Bug Squash, integration, design and collaboration. Treat those as a preparation menu from reported interview patterns, not a guaranteed sequence, timer or private scoring rubric. Your recruiter brief controls the current loop, language, environment and permitted tools.

The focused Stripe Bug Squash guide is useful for practising an unfamiliar codebase. First try the lost-response defect below; then compare how you reproduced and narrowed it.

Which preparation route fits your interview?

Ask recruiting what you will actually do: live implementation, integration against supplied documentation, debugging an existing repository, architecture discussion or project review. Confirm setup instructions and whether you can use local documentation, external search or AI tools. Permission for one resource does not imply permission for another.

Preparation areaWork to demonstrateUseful follow-up
Practical codingA working parser or transformation with testsAdd a malformed record or changed requirement
IntegrationDocumentation-driven pagination and error handlingLose a response after the server commits
Bug SquashReproducer, narrow hypothesis and regressionProve the repair did not hide another defect
Design/implementationContract, data model and a failure pathSeparate provider state from local order state
Refactoring/pairingPreserve behavior while improving a boundaryExplain a change before making it
Project/behavioralYour decisions, evidence and collaborationName a mistake and the corrective action

Junior candidates should finish the core implementation before expanding into distributed guarantees. Experienced backend candidates should also explain concurrency, persistence and reconciliation. If your interview includes an assessment, use its actual invitation for the duration and platform; this article does not supply a universal online-assessment cutoff.

Integration mock: collect paid amounts across two pages

Task: read a fictional payment-list API and return gross succeeded amounts per currency, in integer minor units. The fixture supports only USD and EUR, both with two decimal places. Pending and failed payments are excluded; fees, refunds and currency conversion are outside this contract. These records are simplified practice data, not Stripe's production PaymentIntent schema.

The first call uses a null cursor. The supplied transport represents a v1-style starting_after cursor with the last object ID on the page:

{
  "data": [
    {"id":"p_3","amount":1200,"currency":"usd","status":"succeeded"},
    {"id":"p_2","amount":700,"currency":"usd","status":"pending"}
  ],
  "has_more": true
}

The second call must use p_2, even though that pending record contributes nothing to the total. It returns:

{
  "data": [
    {"id":"p_1","amount":500,"currency":"eur","status":"succeeded"}
  ],
  "has_more": false
}

Expected result: {"usd": 1200, "eur": 500}. Reporting 1,700 as a single money total would silently combine currencies. Advancing with the last succeeded ID instead of the last returned ID can repeat or skip records.

Stripe's current v1 list documentation describes object-ID cursors and a has_more flag. Its v2 interface differs. Read the endpoint's actual version rather than copying this cursor convention into every API.

A complete pagination implementation

The mock transport returns parsed dictionaries, with each row also a dictionary. Validate those shapes at a real HTTP boundary. This function checks the exercise's field values, rejects conflicting duplicate IDs, counts identical duplicates once and stops a repeated cursor or excessive page sequence. On failure it returns no partial total.

def paid_totals(fetch_page, max_pages=20):
    if type(max_pages) is not int or max_pages < 1:
        raise ValueError("positive page budget required")
    cursor, used, seen, totals = None, set(), {}, {}
    for _ in range(max_pages):
        if cursor in used:
            raise ValueError("cursor cycle")
        used.add(cursor)
        page = fetch_page(cursor)
        rows, more = page.get("data"), page.get("has_more")
        if not isinstance(rows, list) or type(more) is not bool:
            raise ValueError("invalid page")
        for row in rows:
            ident, amount = row.get("id"), row.get("amount")
            currency, status = row.get("currency"), row.get("status")
            if (not isinstance(ident, str) or not ident
                or type(amount) is not int or amount < 0
                or currency not in ("usd", "eur")
                or status not in ("succeeded", "pending", "failed")):
                raise ValueError("invalid record")
            value = (amount, currency, status)
            if ident in seen:
                if seen[ident] != value:
                    raise ValueError("conflicting duplicate")
                continue
            seen[ident] = value
            if status == "succeeded":
                totals[currency] = totals.get(currency, 0) + amount
        if not more:
            return totals
        if not rows:
            raise ValueError("empty continuing page")
        cursor = rows[-1]["id"]
    raise ValueError("page budget exhausted")

The seen map makes repeated identical records harmless, but it is not a consistency protocol for a changing dataset. If the same payment arrives with a different amount or status, the exercise raises an error rather than guessing which version wins. A production reconciliation job needs a documented snapshot or change-capture strategy, a stable cutoff and a recovery plan. Dedupe alone cannot prove that a concurrently changing list is complete.

For R returned records and P pages, local processing is O(R + P) with expected constant-time dictionary operations; memory is O(unique IDs). HTTP time and provider sorting are outside that claim. For a very large export, discuss durable checkpoints and bounded storage, while ensuring a restart cannot publish a partially verified financial result.

Retry mock: what if the server commits but the response is lost?

Create a fictional payment using this request:

{"amount":1200,"currency":"usd","order_id":"order_17"}

The path is /mock/payments and the operation key is operation_17. Our fake server stores a receipt, then raises a timeout instead of returning it. A retry with the same path, parameters and key returns that stored receipt. The test verifies one simulated side effect across two transport calls.

The deliberate bug generates operation_17_attempt_0, then operation_17_attempt_1. The server sees two operations and creates two simulated payments. The patch moves key creation out of the retry loop. A key belongs to the intended operation, not a transport attempt; the application must persist that identity before a production request can be retried after a process restart.

StepClient observesFake provider stateAppropriate next action
Initial POSTTimeoutOne receipt already storedKeep order outcome unresolved
Retry, same operation keyStored success receiptStill one paymentRecord confirmed result
Buggy retry, new keyA second success receiptTwo paymentsReconcile the duplicate; do not hide it
HTTP 500 without retry guidanceServer errorSide effect unknownPreserve identity and reconcile

Stripe's idempotency documentation describes storing the first executed request's result, including a 500, and detecting changed parameters. Keys can be removed after they are at least 24 hours old; do not treat provider retention as permanent deduplication. Its advanced error guidance treats 500 outcomes as indeterminate. Those details matter more than saying “retry every error.”

A bounded retry implementation for the fake transport

send returns a normalized dictionary containing integer status, a parsed body on success, and an optional boolean should_retry hint. The adapter must validate those fields; a real header arrives as text. pause is injected so tests collect the delay schedule without waiting. The code freezes a JSON-compatible payload before the first call and reconstructs it for each attempt.

import json

class Indeterminate(RuntimeError):
    pass

def create_with_retry(send, path, payload, key, pause, attempts=3):
    if not isinstance(key, str) or not key:
        raise ValueError("stable operation key required")
    if type(attempts) is not int or attempts < 1:
        raise ValueError("positive attempt budget required")
    encoded = json.dumps(payload, sort_keys=True)
    for n in range(attempts):
        try:
            reply = send(path, json.loads(encoded), key)
        except TimeoutError:
            if n + 1 == attempts:
                raise Indeterminate("no definitive response")
        else:
            status = reply["status"]
            if 200 <= status < 300:
                return reply["body"]
            hint = reply.get("should_retry")
            if hint is False:
                if status >= 500:
                    raise Indeterminate("server outcome unresolved")
                raise RuntimeError("request rejected")
            if hint is not True and status >= 500:
                raise Indeterminate("reconcile; do not change key")
            if hint is not True and status != 429:
                raise RuntimeError("request rejected")
            if n + 1 == attempts:
                if status >= 500:
                    raise Indeterminate("server outcome unresolved")
                raise RuntimeError("retry budget exhausted")
        pause(0.1 * 2 ** n)

The mock's policy retries timeouts, default 429 responses and explicit positive retry hints, with a three-attempt budget. An explicit negative hint stops retrying. Without a positive hint, a 5xx raises Indeterminate; other non-success responses stop the request. Exceptions here describe client control flow, not a guarantee that no provider side effect occurred.

This is a teaching client, not a replacement for Stripe's maintained SDK. It omits real authentication, request timeouts, response-schema validation, jitter, Retry-After, an overall deadline and production logging. Discuss those before using the pattern in a service. Never log secret keys, card details or full sensitive payment payloads.

The same operation identity must survive reopening the job, not merely three attempts in one function call. Store provider object IDs separately from local order status; a transport timeout should not automatically label the order failed and invite another charge. If the retry budget ends, retain an unresolved state for reconciliation rather than switching keys to force a success response.

What should the integration tests prove?

The displayed functions passed 25 local checks against in-memory fixtures. They include the deliberate bug reproduction; no real API or payment was involved.

BoundarySupplied failureVerified result
PaginationPending record is last on pageNext cursor is p_2
Duplicate inputSame ID, same valuesCounted once
Conflicting inputSame ID, different amountRejected, no partial result
Page contractEmpty continuing page or repeated cursorStops with an error
Record contractBoolean/negative amount or unsupported valueRejected
Lost responseProvider stores receipt, client times outSame request retries; one side effect
Retry policy400, 401 or 402, without retry hintOne call, no blind loop
Ambiguous server result500 without positive hintIndeterminate; identity retained
Retry boundsThree timeouts or three 429sThree calls, only 0.1/0.2 s delays

Check the call trace, not only the returned object. A test returning “success” can miss that the client sent two different keys. Also check the cursor sequence and all currency totals. These tests establish local exercise behavior; they do not establish provider durability, concurrent deduplication or exactly-once distributed execution.

How to approach Bug Squash and design follow-ups

Start by running the failing test and reading the project instructions. Follow one request from the public entry point through state mutation and response handling. State a hypothesis that a small probe can disprove. In the retry bug, capture path, sanitized parameters and operation identity across attempts; that evidence distinguishes identity misuse from a faulty total or a pagination problem.

Repair the narrow boundary, rerun the reproducer and add a regression. Explain why the new test fails on the old implementation. Avoid rewriting the transport, parser and state machine together; you would lose the evidence connecting the defect to the change. In pairing, ask whether the other person agrees with your contract before expanding the patch.

For design, extend the same example: a durable order row records the intended operation and current state; a worker sends the provider request; a receipt or reconciliation event updates the order. A transactional outbox can prevent losing scheduled work between a local database write and message publication. It cannot make an unrelated external payment atomic with your database.

Authenticate webhook events, deduplicate deliveries and use allowed state transitions. Do not let an older event turn a confirmed final state back into a pending state merely because it arrived later. These are follow-up design requirements, not features implemented by the small Python fixture.

Prepare real project evidence and a two-week work plan

Stripe's current careers compatibility page emphasizes users, rigorous reasoning and collaboration. Use it to choose truthful examples, not to invent private interview weights. Prepare a story about a user-visible failure, a decision you changed after feedback and a delivery trade-off. Describe what you personally did, what teammates owned, the evidence available and what remains uncertain. Name the consequence before listing technologies.

The following schedule is a suggested study plan; it does not promise interview readiness in fourteen days.

DaysWorkCompletion evidence
1–2Parse and group business dataWorking result, malformed-input tests, complexity explanation
3–4Extend the pagination mockBoth pages, correct cursor, duplicate and cycle checks
5–7Debug the new-key retry defectTwo-effect reproducer, one-effect repair, call-trace test
8–10Rebuild integration from the contractRetry/error fixtures, README and unanswered questions
11–12Design order recoveryLocal/provider state diagram and ambiguous-result timeline
13–14Run one 45-minute mock and project reviewFinished core path, test record and specific next drill

For the mock, reserve roughly five minutes to clarify, twenty-five to implement, ten to test and five to explain limits. Those are practice allocations, not Stripe round timings. If you repeatedly fail to finish the baseline, reduce the scope before adding more topics.

Sources and research scope

Public documentation was checked October 8, 2026. Round names are preparation context retained from the existing guide, not a new official hiring specification. Code, payment fixtures, bug reproduction and the study schedule are original editorial exercises.