Affirm

Affirm Software Engineer Phone Screen Questions

16+ questions from real Affirm Software Engineer Phone Screen rounds, reported by candidates who interviewed there.

16
Questions
8
Topic Areas
10+
Sources

What does the Affirm Phone Screen round test?

The Affirm phone screen typically lasts 45-60 minutes and evaluates core Software Engineer fundamentals. Candidates should expect 1-2 algorithmic problems, basic system design discussion at senior levels, and questions about relevant experience. The goal is to confirm technical competence before bringing candidates onsite.

Top Topics in This Round

Affirm Software Engineer Phone Screen Questions

Just one question, but it's very detailed. The following content requires a score higher than 188. You can already view it. Hit counter. Similar to the one on LC. Please give me some points. Thank you

## Problem Design a class hierarchy for a simple turn-based card game. A standard deck has 52 cards (4 suits, 13 ranks). Players are dealt a hand. On each turn, a player plays one card; the highest-rank card wins the trick. Implement: ```python class Card: suit: str # HEARTS, DIAMONDS, CLUBS, SPADES rank: int # 2-14 (14 = Ace) class Deck: def __init__(self): ... # 52 cards def shuffle(self, seed: int): ... def deal(self, n: int) -> list[Card]: ... class Hand: def play(self, card: Card): ... def has_card(self, card: Card) -> bool: ... class Game: def __init__(self, num_players: int): ... def play_round(self) -> str: # returns winner player id ``` ## Follow-ups 1. How do you enforce that `deal` raises an error when the deck runs out of cards? 2. How would you add a `Trump` suit that beats all other suits regardless of rank? 3. Describe how you would serialize mid-game state to JSON for a save/resume feature. 4. How do you handle games with wildcards (jokers) that can substitute for any card?

## Problem A payment dispute follows this lifecycle: ``` OPENED -> EVIDENCE_SUBMITTED -> UNDER_REVIEW -> RESOLVED_WON -> RESOLVED_LOST -> WITHDRAWN ``` Invalid transitions (e.g., UNDER_REVIEW -> OPENED) must raise an error. Implement: ```python class Dispute: def __init__(self, dispute_id: str): self.status = "OPENED" def transition(self, new_status: str) -> None: # raises ValueError if transition is invalid def is_resolved(self) -> bool: def history(self) -> list[str]: # all statuses in order ``` ## Example ``` d = Dispute("d123") d.transition("EVIDENCE_SUBMITTED") d.transition("UNDER_REVIEW") d.transition("RESOLVED_WON") d.history() -> ["OPENED","EVIDENCE_SUBMITTED","UNDER_REVIEW","RESOLVED_WON"] d.transition("WITHDRAWN") # raises ValueError ``` ## Follow-ups 1. How would you represent the valid transitions as a data structure rather than hard-coded conditionals? 2. Add timestamps to each transition and a method `time_in_state(status)` that returns the duration. 3. How would you persist dispute state to a database and handle concurrent updates? 4. Extend to support automatic escalation: if `UNDER_REVIEW` for more than 30 days, auto-transition to `RESOLVED_LOST`.

## Problem You are given a string representing a custom config file. The format uses indentation (2 spaces per level) to represent nesting, and `key: value` pairs. Values may be strings or nested sections. Parse it into a nested Python dict. ```python def parse_config(text: str) -> dict: ``` ## Example ``` text = """ database: host: localhost port: 5432 credentials: user: admin password: secret server: port: 8080 """ Output: { "database": { "host": "localhost", "port": "5432", "credentials": {"user": "admin", "password": "secret"} }, "server": {"port": "8080"} } ``` ## Follow-ups 1. How do you handle tabs vs. spaces, or mixed indentation? 2. What if values can be lists (lines starting with `- `)? 3. Write a `serialize(d: dict) -> str` function that is the inverse of `parse_config`. 4. How would you add line-number error reporting for malformed input?

## Problem Design a data structure that supports insert, delete, and getRandom in O(1) average time. ## Likely LeetCode equivalent LC 380 - Insert Delete GetRandom O(1) ## Tags Coding, hash_table, arrays, randomization, phone

## Problem Design a loan management system. A loan has a principal, annual interest rate, and term in months. Payments reduce the outstanding balance. ```python class Loan: def __init__(self, principal: float, annual_rate: float, term_months: int): ... def monthly_payment(self) -> float: # fixed amortized payment def make_payment(self, amount: float, month: int) -> None: def outstanding_balance(self, after_month: int) -> float: def is_paid_off(self) -> bool: def payment_schedule(self) -> list[dict]: # [{month, payment, principal_paid, interest_paid, balance}] ``` Use the standard amortization formula: `M = P * r*(1+r)^n / ((1+r)^n - 1)` where `r = annual_rate/12`. ## Example ``` loan = Loan(10000, 0.06, 12) # $10K at 6% for 12 months loan.monthly_payment() -> ~860.66 loan.outstanding_balance(after_month=6) -> ~5136.98 ``` ## Follow-ups 1. How does an early payoff (larger payment) affect the remaining schedule? 2. Add a `LoanPortfolio` class that manages multiple loans and reports total outstanding debt. 3. How would you handle variable interest rates (rate changes per quarter)? 4. Design the database schema to store loan state and payment history.

## Problem Design a `PermanentStack` class that behaves like a standard stack but never truly destroys data. When you `pop()` an element, it is moved to an internal "archive" rather than discarded. All previously popped elements must remain retrievable. ```python class PermanentStack: def push(self, val: int) -> None: ... def pop(self) -> int: ... # removes from active, archives it def peek(self) -> int: ... # top of active stack def restore(self) -> int: ... # moves most-recently archived back to active def get_archive(self) -> list[int]: ... # all archived values, oldest first ``` **Example:** ``` push(1), push(2), push(3) pop() -> 3 (archive: [3]) pop() -> 2 (archive: [3, 2]) restore() -> 2 (active top: 2, archive: [3]) get_archive() -> [3] ``` ## Follow-ups - What if `restore()` must reinstate elements in LIFO order from the archive? - How would you implement `undo_all()` to move every archived element back in original push order? - What are the time and space complexities of each operation? - How would you persist this structure to disk so it survives a process restart?

## Problem Given a list of purchase events for a single user, each as `(item_id, timestamp)`, determine the most frequent consecutive purchase pattern of length `k`. A pattern is a sequence of exactly `k` distinct items bought in consecutive transactions (no gaps). Return the pattern as a list of item IDs in the order they were purchased. If there is a tie, return the lexicographically smallest sequence. ```python def most_frequent_pattern(purchases: list[tuple[int, int]], k: int) -> list[int]: ... ``` **Example:** ``` purchases = [(1,100),(2,200),(3,300),(1,400),(2,500),(3,600),(1,700),(4,800)] k = 2 Output: [1, 2] # (1->2) appears twice; (2->3) appears twice -> tie -> lex smaller ``` **Constraints:** `1 <= k <= len(purchases)`, timestamps are strictly increasing. ## Follow-ups - How does your approach change if the same item can appear multiple times within a pattern? - Extend to find patterns across multiple users and return patterns seen by at least `m` users. - What is the time complexity? Can you do it in O(n) for k=2?

## Problem Compress a string by replacing consecutive repeated characters with the character followed by its count. ## Likely LeetCode equivalent LC 443 - String Compression ## Tags Coding, strings, two_pointers, phone

## Problem Two strings are "fingerprint-equivalent" if one can be obtained from the other by consistently renaming characters. That is, there exists a bijection between the character sets such that applying it to every position of string A produces string B. Given a list of strings, group them by fingerprint equivalence class and return the groups. ```python def group_by_fingerprint(words: list[str]) -> list[list[str]]: ... ``` **Example:** ``` Input: ["aab", "xxy", "abc", "xyz", "bba"] Output: [["aab", "xxy", "bba"], ["abc", "xyz"]] ``` Explanation: "aab" -> normalize as 0,0,1; "xxy" -> 0,0,1; "bba" -> 0,0,1. "abc" -> 0,1,2; "xyz" -> 0,1,2. ## Approach Normalize each string by mapping each first-seen character to an incrementing integer. Use the normalized tuple as a hash key. ## Follow-ups - Does your fingerprint function handle strings with overlapping character ranges across inputs? - How would you extend this to work on sequences of integers rather than characters? - What if two strings of different lengths should never be in the same group -- is that guaranteed, and where in your code does that hold?

## Problem Design a `TransactionLog` system that records financial transactions and supports querying. ```python class TransactionLog: def record(self, txn_id: str, account: str, amount: float, ts: int) -> None: ... def get_balance(self, account: str, as_of: int) -> float: ... def get_transactions(self, account: str, start: int, end: int) -> list[dict]: ... def rollback(self, txn_id: str) -> bool: ... ``` - `record`: append a new transaction (positive = credit, negative = debit). - `get_balance`: sum all transactions for `account` with `ts <= as_of`. - `get_transactions`: return all transactions for `account` in time range `[start, end]`, sorted by ts ascending. - `rollback`: mark a transaction as voided; it no longer affects balance or appears in queries. Return False if txn_id not found. **Example:** ``` record("t1", "alice", 500.0, 1) record("t2", "alice", -200.0, 2) get_balance("alice", 2) -> 300.0 rollback("t2") get_balance("alice", 2) -> 500.0 ``` ## Follow-ups - How would you support multi-account transfers atomically? - What index structure gives O(log n) point-in-time balance queries? - How do you handle concurrent `record` and `rollback` calls safely?

## Problem You are given a list of bilateral transactions between parties: each entry is `(payer, payee, amount)`. Simplify the settlement so the total number of transfers is minimized while each party ends up with the same net balance. ```python def minimize_transfers(transactions: list[tuple[str, str, int]]) -> list[tuple[str, str, int]]: ... ``` **Example:** ``` Input: [("A","B",10), ("B","C",5), ("C","A",5)] Output: [("A","B",5)] # net: A owes 5 to B; B and C are square ``` **Constraints:** amounts are positive integers; up to 20 parties. ## Approach Compute net balance per party. Separate into creditors (positive) and debtors (negative). Greedily match the largest debtor with the largest creditor, emit a transfer, reduce both, and repeat. ## Follow-ups - Prove or disprove that the greedy approach always produces the minimum number of transactions. - How does the problem change if there are currency conversion fees between certain pairs of parties? - What is the time complexity of the greedy solution as a function of number of parties?

## Problem Likely involves a key-value store where retrieval is weighted by associated weights, requiring random weighted selection. ## Tags Coding, hash_table, randomization, phone

## Problem A decision tree is stored as a list of nodes. Each node has: `feature_index` (int or None for leaf), `threshold` (float or None), `left` (child index), `right` (child index), `value` (class label, set only on leaves). Given this structure and a feature vector, implement `predict`. ```python class TreeNode: feature_index: int | None threshold: float | None left: int | None right: int | None value: int | None # only on leaf def predict(nodes: list[TreeNode], features: list[float]) -> int: # traverse from node 0; go left if features[node.feature_index] <= threshold ``` ## Example ``` # Tree: if x[0] <= 3.5 -> predict 0, else if x[1] <= 7.0 -> predict 1, else predict 2 features = [5.0, 8.0] Output: 2 ``` ## Follow-ups 1. How would you implement `predict_proba` that returns class probabilities instead of a hard label? 2. How do you handle missing feature values at inference time? 3. Extend to a random forest: given `n` trees, return the majority vote class. 4. Describe how you would serialize and load this tree from a JSON file.

## Problem Implement a simplified in-memory DOM tree. Each node has a `tag`, optional `id`, a list of `classes`, and child nodes. ```python class DOMNode: def __init__(self, tag: str, id: str = "", classes: list[str] = []): ... def append_child(self, child: 'DOMNode') -> None: ... def query_selector(self, selector: str) -> 'DOMNode | None': # supports: "tag", "#id", ".class", "tag.class" def query_selector_all(self, selector: str) -> list['DOMNode']: def remove_child(self, child: 'DOMNode') -> bool: ``` ## Example ```javascript // equivalent structure: // <div id="root"> // <p class="intro">...</p> // <span class="intro highlight">...</span> // </div> root.query_selector(".intro") // -> first <p> root.query_selector_all(".intro") // -> [<p>, <span>] root.query_selector("#root") // -> root div ``` ## Follow-ups 1. How would you support a descendant combinator, e.g., `"div p"` (all `<p>` inside `<div>`)? 2. Implement `innerHTML` that serializes the subtree back to an HTML string. 3. What traversal strategy does `query_selector` use? Why BFS vs. DFS matters here. 4. How do you handle `query_selector` on a very deep tree (10K levels) without stack overflow?

## Problem You receive a list of raw order log lines. Each line has format: ``` "<timestamp_iso> ORDER_<event_type> order_id=<id> [key=value ...]" ``` Event types: `CREATED`, `PAID`, `SHIPPED`, `DELIVERED`, `CANCELLED`. Parse the logs and produce a summary: ```python def summarize_orders( log_lines: list[str] ) -> dict[str, dict]: # returns {order_id: {"status": str, "events": list, "duration_minutes": int | None}} # duration = minutes from CREATED to DELIVERED (None if not delivered) ``` ## Example ``` logs = [ "2024-01-10T10:00:00 ORDER_CREATED order_id=X1 amount=50.0", "2024-01-10T10:05:00 ORDER_PAID order_id=X1 method=card", "2024-01-10T10:30:00 ORDER_DELIVERED order_id=X1", ] Output: {"X1": {"status": "DELIVERED", "events": [...], "duration_minutes": 30}} ``` ## Follow-ups 1. How do you handle log lines that arrive out of order by timestamp? 2. What if `order_id` appears in `CREATED` for the same ID more than once (duplicate events)? 3. Write a SQL version: given a table `order_events(order_id, event_type, event_time)`, compute the same summary. 4. Identify orders that took more than 2 hours from CREATED to DELIVERED.

What to Expect in the Affirm Phone Screen Round

The Affirm Software Engineer Phone Screen round has a specific calibration purpose distinct from other rounds in the loop. Across 16+ verified reports on LeakCode for this exact round type, the consistent expectations: clear scoping of the problem before diving into a solution, explicit reasoning about complexity, structured handling of edge cases, and the ability to discuss trade-offs between two reasonable approaches.

Reports tagged with the Phone Screen round at Affirm show recurring patterns in difficulty and topic distribution. The Phone Screen round is typically 45-60 minutes; the interviewer is calibrated against a specific rubric. The discriminator between candidates who advance and candidates who do not is rarely the final correctness of the answer. It is the path: did you clarify, did you verbalize your approach, did you handle edge cases, and did you communicate throughout.

How To Prepare for This Specific Round

Filter the questions below to the most recent reports (past 6-12 months). Questions tagged for this exact round type from this exact company at this exact role level are the highest-signal data available. Older reports may reference questions that have since rotated out of the company's pool.

Practice 4-6 representative problems from this set under timed conditions. The goal is not memorization (companies rotate questions); the goal is to internalize the patterns the interviewer typically reaches for and the depth of follow-up to expect. Reports on LeakCode also tag the typical follow-up depth at this round type, which is the discriminating signal between hire and no-hire calibration.

Phone Screen Round Timing and Format

The Phone Screen round at Affirm typically runs 45-60 minutes. Use the first 2-3 minutes to clarify requirements; you should never start coding or designing without verifying the input/output format, constraints, and edge cases out loud. Use the next 5-7 minutes to verbalize your approach before writing any code. The middle 20-30 minutes are implementation. Reserve the final 10 minutes for testing with concrete examples and discussing optimization or trade-offs.

Time budget discipline is one of the most reliable senior-vs-junior discriminators in this round. Strong candidates verbalize where they are in their budget out loud ("I've used about 20 minutes, I have 15 minutes left for testing and one optimization"). This signals engineering maturity to the interviewer and creates positive feedback they can capture in writing.

Common Failure Modes in This Round

Reports tagged "no hire" at Affirm Software Engineer Phone Screen commonly cite: coding silently without verbalizing approach, jumping to implementation before clarifying requirements, missing edge cases (empty input, single element, very large input), producing working code that the candidate cannot refactor when asked, and failing to test their solution with concrete examples before declaring done.

The single most predictive failure mode in 2025-2026 reports: not asking clarifying questions. Interviewers at all FAANG companies are explicitly trained to weight this dimension. Strong candidates ask 3-5 clarifying questions even on problems that look obvious; weak candidates dive into code immediately. The clarifying-question check is often the first signal recorded in the interviewer's notes.

See All 16 Questions from This Round

Full question text, answer context, and frequency data for subscribers.

Get Access