Affirm Software Engineer Interview Experience — Fraud Detection Coding and a Payment System Design Round

Affirm·Software Engineer·Apr 2026
Onsitemedium

Four rounds total: one hour of coding + half an hour of behavioral questions + one hour of system design + half an hour of behavioral questions.

The two behavioral rounds were just the standard questions.

Coding round (the prompt was long, but not hard):

Part 1: Live Fraud Detector (Debug Existing Function)

Description

One of Affirm's competitive edges is our ability to do credit underwriting and detect potential fraud quickly. We've put a great deal of engineering work into optimizing every part of the process, and our merchant partners rely on us to reliably produce a decision in seconds.

We're building out a new event-driven architecture, where we have event consumers listening to a stream of underwriting and fraud events. The DistinctPIIValuesCounter is one such event consumer, and it's responsible for counting the total number of Personally Identifiable Information (PII) values that are present in all events with type underwriting.

We've noticed that DistinctPIIValuesCounter has been misbehaving recently and producing incorrect results. It's likely that the code has one or more bugs in it. We'd like you to investigate the code and help fix this part of the system!

To simulate the event stream, we will pass in a JSON list of objects (dictionaries), with each dictionary representing a single event in the stream. The handle_event method will be called sequentially for each of these objects. At the end, we will call the get_total_unique_pii_values function and assert that the correct number of unique PII values was counted.

Example (Test Case 0)

event_stream = [
    {
        "event_type": "underwriting",
        "customer_details": {
            "address": "address-A",
            "phone": "phone-A",
            "email": "email-A",
            "ssn": "ssn-A"
        },
        "loan_amount": 3000
    },
    {
        "event_type": "fraud_flag",
        "customer_details": {
            "address": "address-A",
            "phone": "phone-A",
            "email": "email-A",
            "ssn": "ssn-A"
        }
    },
    {
        "event_type": "underwriting",
        "customer_details": {
            "address": "address-A",
            "phone": "phone-B",
            "email": "email-B",
            "ssn": "ssn-A"
        },
        "loan_amount": 3000
    }
]

assert event_consumer.get_total_unique_pii_values() == 6

Explanation: there are 6 distinct PII values across all underwriting-type events: address-A / phone-A / phone-B / email-A / email-B / ssn-A.

Example (Test Case 1)

event_stream = [
    {
        "event_type": "underwriting",
        "customer_details": {
            "address": "address-A",
            "phone": "phone-A",
            "email": "email-A"
        },
        "loan_amount": 3000
    }
]

assert event_consumer.get_total_unique_pii_values() == 3

Function description: complete handle_event and get_total_unique_pii_values in the editor. Returns an int — the total number of unique PII values across all underwriting type events.

Constraint: neither method should exceed a runtime complexity greater than O(log n), where n is the total number of events.

Event schema — required: event_type (string).

underwriting event schema — required: event_type (str: "underwriting"), loan_amount (int), customer_details (dict), customer_details.phone (string, PII); optional: customer_details.email, customer_details.address, customer_details.ssn (all PII strings), customer_details.credit_score (int).

fraud_flag event schema — required: event_type (str: "fraud_flag"), customer_details (dict); optional: customer_details.phone, customer_details.email, customer_details.address, customer_details.ssn (all PII strings).

Buggy starter code:

from dataclasses import dataclass
from typing import Dict, Optional

@dataclass
class Event:
    event_type: str
    loan_amount: Optional[int] = None
    customer_details: Optional[Dict[str, str]] = None

class DistinctPIIValuesCounter:
    def __init__(self):
        self.pii_set = set()

    def handle_event(self, event: Event) -> None:
        details = event.customer_details
        if not details:
            return
        self.pii_set.add(details.get("address", ""))
        self.pii_set.add(details.get("phone", ""))
        self.pii_set.add(details.get("email", ""))

    def get_total_unique_pii_values(self) -> int:
        return len(self.pii_set)

(The bugs: it doesn't filter out events where event_type != "underwriting"; it's missing the ssn field; and missing fields get added as empty strings "" to the set, which inflates the count. The fix I show in my screenshot is correct: return early for non-underwriting events, then loop over ["address", "phone", "email", "ssn"] and use and value to filter out empty values.)

Part 2: Live Fraud Detector (Coding)

Description

We'd like to implement FraudDetector, which will be the backbone of our system.

We'll be using an event stream, and operate on two different types of events: "underwriting" events, and "fraud_flag" events, following the schema above.

"underwriting" events represent a live transaction that's being underwritten, so the handle_event method needs to return "1" if we believe fraud is suspected, or "0" if we don't detect any fraud.

"fraud_flag" events represent an instance where a manual operator has identified a past transaction as fraudulent. The handle_event method should return an empty string when it handles a fraud_flag event.

FraudDetector will be responsible for identifying subsequent "underwriting" transactions that may be fraudulent, and returning "1" for these suspicious transactions. If an underwriting event includes any customer PII that has been found in at least one suspicious event, we consider it suspicious too.

(Same two event schemas as Part 1.)

Example / Test Case 0

[
    {
        "event_type": "underwriting",
        "customer_details": {
            "address": "address-A",
            "phone": "phone-A",
            "email": "email-A",
            "ssn": "ssn-A"
        },
        "loan_amount": 3000
    },
    {
        "event_type": "fraud_flag",
        "customer_details": {
            "address": "address-A",
            "phone": "phone-A",
            "email": "email-A",
            "ssn": "ssn-A"
        }
    },
    {
        "event_type": "underwriting",
        "customer_details": {
            "address": "address-A",
            "phone": "phone-B",
            "email": "email-B",
            "ssn": "ssn-A"
        },
        "loan_amount": 3000
    },
    {
        "event_type": "underwriting",
        "customer_details": {
            "address": "address-B",
            "phone": "phone-B",
            "email": "email-C",
            "ssn": "ssn-B"
        },
        "loan_amount": 9000
    }
]

Expected results:

0   # no fraud detected
    # not an underwriting event, return blank line
1   # suspicious underwriting event!
1   # suspicious underwriting event!

Explanation: the first underwriting event passes and returns False, because none of its details are considered suspicious yet. The second event marks the following values as suspicious and records them for future reference: "address-A", "phone-A", "email-A", "ssn-A". The third event has an address and ssn that are flagged as suspicious, so we return "1" — its details are also recorded as suspicious for future underwriting events. The fourth event has a phone number that's suspicious (flagged in event 3).

Starter code:

@dataclass
class Event:
    event_type: str
    loan_amount: Optional[int]
    customer_details: Optional[Dict[str, str]]

class FraudDetector:
    def __init__(self):
        return

    def handleEvent(self, event: Event) -> str:
        return ""

Note: point 3 of the Part 2 explanation is the key thing — the PII from a suspicious underwriting event also needs to get recorded into the suspicious set, contagion-style, not just the PII from fraud_flag events.

System design round:

Billing / UserComm Systems Task: Implement a Payment Processing System for Installment Loans

Context: you are building a repayment system for installment loans. Assume the user already exists and has an active loan. You can stub out APIs for users and loans as needed.

Objective: design and implement a system (or set of systems) to handle the processing of loan repayment transactions and user notifications. Specifically, the system must:

  • Accept payment requests, either initiated by the user through an app or website, or triggered automatically via scheduled payments.
  • Interact with the user's bank using a synchronous debit API, which is only operational for 1 hour per day (simulating ACH debit constraints).
  • Notify the end-user of the payment result (success or failure) via SMS or email.

Published

Curated and edited by PracHub

Practice the questions from this interview

Discussion

Sign in to join the discussion. The author is notified of every comment.

Loading comments…

Interview at a glance

Company
Affirm
Role
Software Engineer
Rounds
Onsite
Difficulty
medium
Interview date
Apr 2026
Questions from this interview
3 questions

Real Affirm interview experiences

First-hand reports from Affirm candidates — the rounds, the questions they were asked, and how it went.

All 6 Affirm interview experiences