Affirm Software Engineer Interview Prep Guide
Everything Affirm actually asks Software Engineer candidates — concept walkthroughs, worked examples, and the real interview questions, drawn from candidate reports. Free to read.
Last updated

Focus most on system design: you self-rated it 2/5, selected sharding, distributed scheduling, rate limits, offline sync, and transactional integrity, and have only light engagement signals there. Merely review the coding, data manipulation, production-quality coding, and behavioral topics, since you have viewed several of those areas and did not flag them as weak. The Affirm-specific emphasis is on durable payment idempotency, ledger correctness, rate-limited borrower notifications, partitioned financial data, scheduled payment jobs, and conflict-safe sync. With under a week, this plan keeps breadth across technical screen and onsite while spending the extra minutes on the system-design gaps most likely to matter.
Technical Screen — 51 min
System Design
-
Notification And Rate Limiter Service Design (Focus) — covered in depth under Onsite below.
-
Affirm Financial Data Sharding And Partitioning (Focus) — covered in depth under Onsite below.
-
Distributed Job Scheduling For Payment Workflows (Focus) — covered in depth under Onsite below.
-
Offline Sync And Conflict Resolution For Merchant State (Focus) — covered in depth under Onsite below.
Coding & Algorithms
Affirm-relevant data handling topic; no low rating given, but worth practicing order-dependent fraud rules.

What's being tested
These problems test set-based deduplication, event-stream processing, field validation, and preserving temporal order. You must distinguish underwriting events from fraud flags, normalize eligible values, and update shared state at the correct point in processing.
Patterns & templates
-
Use a global
setfor distinct nonempty PII values; membership and insertion are expectedO(1)average, withO(U)space forUunique values. -
Iterate events once in input order; maintain state incrementally rather than rescanning prior events, yielding
O(E × F)time forEevents andFfields. -
Centralize recognized-field handling with an allowlist such as
{"address", "phone", "email", "ssn"}; ignore unknown keys explicitly. -
Normalize only as specified: exclude
null, missing, and empty-string values, but do not silently alter formatting unless the prompt requires it. -
For fraud propagation, check an underwriting event’s PII against the suspicious
setbefore adding its values; ordering changes the result. -
Separate event classification from value extraction with helpers like
is_underwriting(event)andextract_pii(event)to reduce branching and testing complexity. -
Preserve original event order; sorting can violate causality and incorrectly allow later fraud information to affect earlier underwriting decisions.
Common pitfalls
Pitfall: Counting fields or events instead of globally distinct values; the same email appearing repeatedly contributes only once.
Pitfall: Marking an underwriting event suspicious after inserting its own PII, which incorrectly makes self-introduced values trigger fraud.
Pitfall: Treating fraud flags as ordinary PII sources, or allowing missing, empty, and unrecognized fields into the set.
Practice these
The practice cards below cover the canonical variants — solve all of them and time yourself.
Practice questions
Data Manipulation (SQL/Python)
- Event Filtering, Deduplication, And Time Windowing — covered in depth under Onsite below.
Software Engineering Fundamentals
- High-Card Game Rules Engine — covered in depth under Onsite below.
Behavioral & Leadership
- Behavioral Ownership And Cross-Functional Leadership — covered in depth under Onsite below.
Onsite — 45 min
System Design
Focus area — You explicitly selected rate-limiting and quotas, so this becomes a primary system-design target.

What's being tested
This design tests whether you can decompose a notification platform and a distributed rate limiter into clear APIs, storage models, asynchronous workflows, and failure boundaries. The interviewer is probing your command of consistency, idempotency, backpressure, retries, and multi-tenant isolation—not merely whether you can name `Redis` and `Kafka`. For Affirm-like financial workflows, correctness matters: duplicate, delayed, or missing notifications can create user confusion, while an ineffective limiter can overload downstream providers or allow abusive traffic.
Core knowledge
-
Requirements clarification should establish notification channels (
`email`,`SMS`, push), delivery guarantees, expected throughput, latency targets, retention, tenant/user quotas, and whether ordering is required. Separate business-critical notifications from best-effort marketing or informational messages. -
Use an API gateway or service endpoint for synchronous validation, then persist an immutable notification request before returning success. A durable outbox pattern prevents the classic failure where the database commit succeeds but publishing the asynchronous event fails.
-
Model the workflow as a durable state machine:
`accepted` → `queued` → `sending` → `delivered`, with terminal states such as`failed`,`expired`, or`suppressed`. Store provider message IDs, attempt counts, timestamps, and failure classifications for reconciliation and debugging. -
Use a durable queue such as
`Kafka`,`SQS`, or`Pub/Sub`between intake and delivery workers. Partitioning by recipient or notification key can preserve per-user ordering, but global ordering is expensive and usually unnecessary. -
Delivery is generally at least once, so consumers must be idempotent. Require a caller-supplied idempotency key or derive a stable event ID; enforce uniqueness in
`Postgres`or a distributed key store before sending. Exactly-once external delivery is usually impossible without provider cooperation. -
Implement retries with exponential backoff and jitter, for example . Retry transient network errors, provider
`429`, and selected`5xx`; do not blindly retry invalid addresses, revoked tokens, or permanent provider errors. -
A token bucket limiter allows bursts while enforcing an average rate: tokens refill at rate up to capacity , and each request consumes one token. A request is accepted when available tokens ; otherwise return, delay, or queue it.
-
A sliding-window counter is simpler but less precise at boundaries; fixed windows can permit nearly requests around a reset. For distributed enforcement, store bucket state atomically in
`Redis`using Lua or a server-side transaction, and define clock and TTL behavior explicitly. -
Choose limiter scope deliberately: per API key, merchant, user, destination, provider, channel, and globally. Apply hierarchical limits from narrowest to broadest; an abusive tenant should not consume the entire global SMS capacity.
-
Backpressure prevents overload when downstream providers slow down. Bound queue depth and worker concurrency, expose saturation metrics, and decide whether excess work is rejected, delayed, expired, or routed to a fallback channel. Unbounded queues merely convert overload into latency and eventual memory failure.
-
Provider adapters should normalize responses into an internal contract containing
`accepted`,`retryable`,`permanent_failure`, and`unknown`outcomes. An`unknown`result after a timeout is dangerous: retrying may duplicate delivery, so reconcile using the provider’s idempotency key or status API when available. -
Define observability around operational behavior: acceptance rate, queue age, delivery success rate, retry volume, provider
`429`rate, limiter rejection rate, and`p99`delivery latency. Correlate every request through a trace ID, notification ID, and provider ID without logging message bodies or sensitive financial data.
Worked example — Design a Notification and Rate Limiter Service
A strong candidate first clarifies whether the goal is an API for submitting notifications, a delivery system, a standalone limiter, or all three, then asks about channels, peak traffic, delivery guarantees, ordering, and tenant-specific quotas. They might declare an assumption of 100,000 requests per second at intake, at-least-once processing, per-user ordering, and no guarantee of exactly-once external delivery. The answer can then be organized around four pillars: an idempotent intake API and durable outbox, a partitioned queue with worker state machines, a provider-adapter and retry strategy, and a distributed hierarchical limiter. They should explicitly choose a token bucket in `Redis` with an atomic Lua script for fast admission, while acknowledging that durable queue limits still need enforcement outside the cache. They should flag the tradeoff between strict ordering and throughput: partitioning by user preserves local order but can create hot partitions, so ordering should be limited to notification classes that require it. They should explain that a provider timeout produces an ambiguous result and requires reconciliation rather than unconditional retry. They should close by saying that, with more time, they would detail multi-region behavior, provider failover, key expiration, quota configuration rollout, and a failure-injection test plan.
A second angle
Consider a notification service where the dominant constraint is a provider quota rather than API traffic: `SMS` permits only 500 messages per second, while email has much higher capacity. The limiter must then sit close to each provider adapter and account for channel, destination country, tenant, and provider-specific quotas. A global intake limiter still protects the service, but queue scheduling and weighted fairness become more important than simple request rejection. The design should also address expiration, because a delayed one-time code may be useless even if it eventually sends.
Common pitfalls
Pitfall: Treating a
`Redis`increment as sufficient for distributed rate limiting ignores race conditions, clock behavior, and multi-key hierarchical limits. Use an atomic token-bucket operation, define TTLs, and explain what happens when the limiter store is unavailable.
A tempting answer promises exactly-once notification delivery by combining a database transaction with a queue. That only gives durable intent; the external provider call can still time out after accepting the message. Say “at least once internally, idempotent where supported, and reconciled for ambiguous outcomes” instead.
Candidates often enumerate components without establishing requirements or failure semantics. Start with scale, latency, ordering, quotas, and delivery guarantees, then connect each choice to those assumptions; this demonstrates engineering judgment rather than architecture-name memorization.
Connections
The interviewer may pivot to distributed locks, outbox and inbox patterns, fair scheduling, dead-letter queues, or multi-region consistency. Be ready to compare `Redis`-based admission with gateway-native limiters and to explain how circuit breakers interact with retries and backpressure.
Further reading
-
Designing Data-Intensive Applications — Martin Kleppmann’s treatment of streams, delivery semantics, partitioning, and consistency.
-
The Tail at Scale — why
`p99`latency, hedging, and failure amplification matter in distributed services. -
Amazon Builders’ Library: Timeouts, retries, and backoff with jitter — practical guidance for avoiding retry storms and overload cascades.
Practice questions
Focus area — You selected sharding and partitioning, and financial systems need clear tenant, account, merchant, and time-based partition tradeoffs.

What's being tested
This topic tests whether you can design a distributed storage layer that preserves financial correctness while scaling reads, writes, and operational workload. The interviewer is probing shard-key selection, request routing, cross-shard transaction handling, hot-partition mitigation, and safe resharding—not merely whether you can say “hash by customer ID.” Affirm cares because payment and loan data must remain correct, auditable, available, and isolated even during retries, failures, and traffic spikes.
Core knowledge
-
Sharding distributes rows across independent database partitions, while partitioning may divide data within one database. Explain whether the design needs horizontal write scaling, operational isolation, archival boundaries, or all three.
-
Choose a shard key that matches dominant access paths and spreads load. For customer-centric financial data,
account_idoruser_idoften preserves locality; avoid keys such as merchant category or status that create skew and hot shards. -
Consistent hashing reduces movement when nodes change, but virtual nodes and explicit shard maps are usually easier to reason about for transactional databases. A lookup can be
shard = hash(account_id) mod N, but changingNremaps many accounts unless indirection is added. -
A directory-based router maps a logical tenant or account to a physical shard. Store the mapping durably, cache it with bounded staleness, and define behavior for cache misses, concurrent moves, and a shard-map version mismatch.
-
Keep operations within one shard whenever possible. Cross-shard joins and transactions require two-phase commit, an application-level saga, or an event-driven workflow; each increases latency and introduces partial-failure states.
-
Financial balances should use an append-only double-entry ledger, where every transaction has equal debits and credits: . Materialized balances can be rebuilt or reconciled from ledger entries rather than treated as the sole source of truth.
-
Design retries around idempotency keys. Persist the key with the operation result on the authoritative shard, enforce uniqueness, and return the original result for a duplicate request instead of applying the payment twice.
-
Hot keys arise when one account, merchant, or settlement window receives disproportionate traffic. Mitigations include per-account serialization, optimistic concurrency with version checks, bounded queues, logical sub-buckets, or routing read-heavy projections separately from the write path.
-
Replication improves availability but creates consistency choices. For money movement, route reads requiring read-your-write behavior to the primary or use a session/commit version; never let a stale replica authorize a debit.
-
Resharding should be an online migration: dual-read or dual-write carefully, copy a consistent snapshot, replay changes, validate counts and checksums, quiesce or fence writes briefly, then atomically switch the directory mapping and retain rollback capability.
-
Enforce tenant isolation at multiple layers: routing authorization, database credentials, row-level ownership checks, and audit logging. A correct shard lookup is not sufficient if an attacker can submit another customer’s identifier.
-
Define failure semantics explicitly: shard unavailable, router unavailable, timeout after commit, duplicate retry, and partial cross-shard completion. Use durable operation states such as
PENDING,COMMITTED, andFAILED, with reconciliation rather than ambiguous success.
Worked example
Design a sharded financial ledger for customer accounts
In the first 30 seconds, a strong candidate clarifies transaction volume, read/write ratios, account-to-user cardinality, required availability, whether transfers can cross accounts, and the consistency guarantee for balance reads. They would state an assumption such as “all ledger entries for one account are colocated, but transfers between accounts may span shards.” The answer would be organized around four pillars: shard-key and routing design, single-shard ledger correctness, cross-shard transfer coordination, and failure recovery/observability. They would use an append-only double-entry ledger with a unique idempotency key and balance version, then expose a read model that is rebuilt or reconciled from ledger events. For a cross-shard transfer, they might reserve funds on the source shard, persist a durable transfer state, credit the destination shard, and finalize or compensate based on recovery state. They should explicitly flag the tradeoff between two-phase commit, which offers stronger atomicity but couples shard availability, and a saga, which improves availability but requires visible intermediate states and reconciliation. They would explain that the router uses a versioned account-to-shard directory rather than embedding physical shard numbers in clients. If they had more time, they would detail online resharding, disaster recovery, invariant checks, and load-test scenarios for a hot account.
A second angle
Design account-history queries across many shards
Here the dominant constraint is fan-out latency rather than atomic writes: a query for a user’s complete history may touch multiple account shards and replicas. The service should maintain a bounded parallelism budget, apply per-shard deadlines, merge results by a stable cursor such as (event_time, ledger_id), and avoid returning duplicate or missing pages during concurrent writes. A denormalized user-history index can reduce fan-out, but it introduces asynchronous lag and must expose a clear consistency contract. The same partitioning decision therefore shifts from “keep a transaction local” to “make the read path predictable without sacrificing auditability.”
Common pitfalls
Pitfall: Saying “hash by user ID” without checking access patterns. If transfers, merchant settlement, or compliance queries dominate, the chosen key may force expensive fan-out or create skew; tie the key to concrete request paths and cardinality.
A common analytical mistake is treating replication as a substitute for partitioning. Replicas improve read capacity and failover, but they do not solve a single-shard write bottleneck, cross-shard coordination, or a hot account receiving all writes.
A communication mistake is presenting cross-shard transfers as magically atomic. Say exactly what happens after a timeout, how retries find the prior operation, whether intermediate states are visible, and which invariant or reconciliation process detects an incomplete transfer.
A depth mistake is focusing only on normal routing and ignoring movement. Interviewers often probe resharding, stale router caches, lost acknowledgments, duplicate requests, and a shard failing after the source debit but before the destination credit.
Connections
Expect pivots to consistent hashing, two-phase commit, sagas, idempotency, leader election, and replica consistency. The interviewer may also connect this to database indexing, event sourcing, auditability, rate limiting, and disaster-recovery objectives such as RPO and RTO.
Further reading
-
Designing Data-Intensive Applications — Martin Kleppmann’s treatment of partitioning, replication, transactions, and consistency.
-
Stripe: Designing robust APIs — practical explanation of idempotency keys and safe retry behavior.
-
Google Spanner: “Spanner: Google’s Globally-Distributed Database” — background on distributed transactions, time, and externally consistent databases.
Practice questions
Focus area — You selected distributed job scheduling; Affirm-like scheduled payments require retries, leases, idempotency, and observability.

What's being tested
This topic tests whether you can design a durable distributed scheduler that executes payment-related work reliably despite crashes, retries, duplicate deliveries, clock errors, and partial outages. The interviewer is probing your command of state machines, leases, idempotency, queue semantics, and failure recovery—not merely whether you can put delayed messages into a queue. Affirm cares because a missed, duplicated, or incorrectly ordered payment operation can create customer harm, reconciliation defects, financial loss, and compliance risk.
Core knowledge
-
A job state machine makes legal transitions explicit:
PENDING → CLAIMED → RUNNING → SUCCEEDED, with retryable failures returning toPENDINGand permanent failures becomingDEAD_LETTERED. Store transitions durably with timestamps, attempt counts, and an error category. -
Separate scheduling from execution. A scheduler determines when work is eligible; workers perform the operation. Persist
run_atand query or index due jobs, then publish them to`Kafka`,`SQS`, or a database-backed queue. This lets workers scale independently. -
Use at-least-once delivery as the practical default. A worker can crash after charging a card but before acknowledging the message, so redelivery is expected. “Exactly once” is generally not end-to-end achievable across a queue and an external payment provider; make the operation idempotent instead.
-
An idempotency key should be stable across retries, such as
payment_id + operation_type + logical_attempt, and be accepted by the payment service or stored in an idempotency table. Enforce a unique constraint so concurrent workers cannot create two effects for one logical operation. -
Claim work with a lease, not a permanent lock. An atomic conditional update such as
UPDATE jobs SET owner=?, lease_until=? WHERE id=? AND status='PENDING'prevents two workers from claiming the same job; lease expiry makes abandoned work recoverable. -
Use visibility timeouts or lease renewal for long-running jobs. Set the timeout above normal
p99execution time plus network margin, renew only while healthy, and fence stale workers with a monotonically increasing fencing token checked before committing results. -
Retries need a bounded backoff policy: commonly exponential delay with jitter, for example . Retry timeouts, connection failures, and
5xxresponses; avoid retrying invalid payment details or other known permanent failures. -
Preserve payment-specific ordering where required. Partition by
account_idorpayment_id, or enforce a database sequence such asoperation_number; do not impose global ordering, which limits throughput and creates a single bottleneck. -
Make enqueueing and state changes atomic with the transactional outbox pattern. Write the business-state update and an outbox record in one
`Postgres`transaction; a relay publishes the outbox record and marks it sent. Consumers remain idempotent because the relay can publish duplicates. -
Treat time carefully. Store timestamps in UTC, use a synchronized wall clock only for eligibility, and use a monotonic clock for lease-duration measurement. Define behavior for daylight-saving changes, missed schedules, clock skew, and jobs whose deadline has already passed.
-
Design for horizontal scale with an indexed due-time query such as
(status, run_at), bucketed time wheels, or delayed queues. For very large volumes, shard by job ID or payment ID and avoid repeatedly scanning millions of future jobs; a single scheduler partition is a throughput and availability risk. -
Build operational controls into the design: queue lag, due-job age, execution latency, retry rate, lease expirations, duplicate suppression, dead-letter count, and provider error rate. Add alerts for stuck transitions and reconciliation mismatches, plus a replay tool that requires authorization and preserves the original idempotency key.
Worked example — Design a distributed job scheduler for payment workflows
I would first clarify the workload: expected jobs per second, delay precision, maximum execution time, required ordering, provider behavior, and whether a duplicate external charge is unacceptable. I would state an assumption such as at-least-once delivery with a payment API supporting idempotency keys, because that is safer and more realistic than promising exactly-once execution. I would organize the design around four pillars: a durable job/state store, a scheduler-to-queue handoff, leased workers, and idempotent payment-side effects. The job row would contain run_at, status, attempt count, lease metadata, a stable idempotency key, and a fencing token, while an outbox would make state changes and enqueue requests atomic. Workers would claim jobs conditionally, execute with bounded exponential backoff, record the result, and move unrecoverable work to a dead-letter state. The explicit tradeoff is database polling versus a managed delayed queue: indexed polling is simpler and gives strong visibility, while a delayed queue can scale better but complicates cancellation, inspection, and recovery. I would close by adding reconciliation against the payment provider and, with more time, detail shard rebalancing, provider rate limits, and multi-region failover.
A second angle — Recovering from worker and scheduler failures
A recovery-focused version changes the emphasis from normal scheduling to crash consistency and duplicate prevention. Ask what happens if the scheduler publishes a job and crashes before recording that fact, or if a worker completes a payment immediately before losing its lease. The answer should use an outbox, lease expiry, fencing tokens, and a provider-side idempotency key rather than relying on acknowledgments alone. You should also distinguish safe replay from unsafe replay: replaying a status lookup may be harmless, while replaying a non-idempotent charge requires reconciliation first. This framing tests whether your design remains correct under adversarial failure timing, not just under healthy operation.
Common pitfalls
Pitfall: Claiming “exactly once” because the queue deduplicates messages. Queue-level deduplication does not cover a worker crash after an external payment succeeds; explain at-least-once execution plus idempotent effects.
Pitfall: Saying “use a lock” without specifying ownership, expiry, renewal, and stale-owner behavior. A strong answer names an atomic claim, lease timeout, fencing token, and the database condition that prevents an old worker from committing.
Pitfall: Focusing only on throughput and omitting payment correctness. Discuss permanent versus transient errors, reconciliation, dead-letter handling, audit history, operator replay, and metrics such as due-job age and duplicate suppression.
Connections
The interviewer may pivot to transactional outbox, saga orchestration, leader election, or rate limiting against payment providers. Be ready to compare `Kafka` consumer offsets, database leases, and workflow engines such as `Temporal`, especially around durability, visibility, cancellation, and operational complexity.
Practice questions
Focus area — You selected offline sync and conflict resolution; practice versioning, merge rules, and eventual consistency for financial product state.

What's being tested
This topic tests whether you can design a distributed state synchronization system that remains correct under disconnection, retries, concurrent edits, and stale clients. The interviewer is probing your command of versioning, idempotency, conflict detection, and deterministic resolution—not merely whether you can copy records between devices. Affirm cares because merchant configuration can affect checkout behavior, eligibility, pricing, and servicing; silently overwriting valid state can create financial, compliance, or operational incidents.
Core knowledge
-
Define the consistency model before choosing an algorithm. Offline clients usually require eventual consistency, but sensitive merchant fields may require server serialization, explicit approval, or fail-closed behavior rather than last-write-wins.
-
Model a sync unit as an operation or versioned mutation, not just a full snapshot:
{entity_id, operation_id, base_version, payload, client_id, timestamp}. Operations enable replay, auditability, selective conflict handling, and efficient delta sync. -
Use idempotency keys for every upload. The server stores
operation_idresults or deduplicates through a unique constraint, so retries after timeouts cannot apply the same mutation twice. This is essential because “at least once” delivery is normal. -
A scalar server version detects stale writes: the client sends
base_version = 41, and the server accepts only if the current version is41, then commits version42. This is optimistic concurrency control; reject or merge when the precondition fails. -
Last-write-wins is simple but dangerous. Wall clocks can be skewed, delayed edits can overwrite newer intent, and timestamps do not prove causality. If used, prefer server-assigned ordering and document which fields tolerate data loss.
-
Version vectors represent causality across replicas: each client tracks counters such as
{A: 7, B: 3}. Vectorxdominatesywhen every component is at least as large and one is greater; incomparable vectors indicate concurrent updates. -
CRDTs provide deterministic convergence when the data type supports their merge laws. G-Counters suit monotonic increments, observed-remove sets suit membership, and LWW registers suit simple values; arbitrary business rules often need domain-specific server logic instead.
-
Prefer field-level or entity-level conflict boundaries deliberately. Merging independent fields can preserve more user intent, but may create invalid combinations—for example, a changed currency and unchanged amount. Validate the merged candidate atomically.
-
Maintain tombstones for deletions. Without a durable deletion marker, an old offline client can resurrect a removed merchant record during its next upload. Garbage-collect tombstones only after all relevant replicas have acknowledged a safe synchronization boundary.
-
Use a pull/push protocol with a sync cursor or log sequence number: upload pending mutations, receive accepted results and conflicts, then pull changes after the last acknowledged cursor. Advance the cursor only after durable local application.
-
Separate transport retries from application outcomes. A timeout means “unknown,” not failure; retry with the same idempotency key. Return explicit statuses such as
accepted,already_applied,conflict,invalid, andunauthorized. -
Protect against stale or malicious clients with authorization at apply time, not only at login. Check merchant scope, field permissions, schema version, and business invariants on every mutation; never allow offline mode to bypass server-side policy.
Worked example
Representative design prompt: Design offline sync and conflict resolution for merchant state. In the first 30 seconds, I would clarify which merchant fields are editable offline, whether multiple users or devices can edit concurrently, whether deletion is supported, and which fields are too sensitive to merge automatically. I would state that the server is authoritative, clients may queue mutations while disconnected, delivery is at least once, and the system must never silently lose a valid financial configuration. I would organize the answer around the local mutation queue, the server-side versioned write path, the pull/push synchronization protocol, and conflict resolution plus observability. Each mutation would carry an idempotency key, entity version, client identity, schema version, and operation payload; the server would atomically validate authorization, compare the base version, apply or reject the mutation, and append an audit event. A key design decision is whether to use full-record last-write-wins or field-level merging; I would choose explicit conflict responses for high-risk fields and narrowly scoped merges only where invariants can be checked. Clients would persist accepted changes and unresolved conflicts locally, retry unknown outcomes safely, and use tombstones to prevent deleted state from reappearing. I would close by saying that, with more time, I would specify the wire protocol, migration behavior, compaction rules, and failure-injection tests for duplicate, reordered, and partially applied messages.
A second angle
The same design changes when the state is a merchant catalog or availability set rather than a payment-critical configuration. An observed-remove set or operation-based CRDT may be appropriate because independent additions can merge automatically and temporary divergence is acceptable. The candidate should still define deletion semantics, duplicate operation handling, and what happens when two devices modify the same item concurrently. The key contrast is that the business risk permits more automatic convergence, while sensitive configuration should favor explicit server rejection and human resolution.
Common pitfalls
Pitfall: Treating timestamps as a reliable ordering mechanism is analytically wrong under clock skew and offline delays. Use server versions, causal metadata, or explicitly acknowledge the data-loss tradeoff of last-write-wins.
A tempting implementation answer is “the client retries the request if it times out.” That is incomplete because the server may have committed successfully before the response was lost. Explain idempotency keys, durable deduplication, and the distinction between an unknown outcome and a rejected operation.
Many candidates jump directly to CRDTs as a universal solution. CRDTs solve convergence for suitable data types, not authorization, cross-field invariants, audit requirements, or business-specific conflict semantics; demonstrate where a transactional server-side merge is safer.
Connections
Expect pivots to optimistic concurrency control, event sourcing, message delivery semantics, schema evolution, and disaster recovery. You may also need to discuss how sync state is tested with deterministic replay, fault injection, and property-based convergence checks.
Further reading
-
Designing Data-Intensive Applications — Martin Kleppmann’s treatment of replication, version vectors, CRDTs, and consistency tradeoffs.
-
A Comprehensive Study of Convergent and Commutative Replicated Data Types — formal foundations for CRDT convergence and merge behavior.
-
Dynamo: Amazon’s Highly Available Key-value Store — practical discussion of eventual consistency, vector clocks, sloppy quorums, and conflict resolution.
Practice questions
Coding & Algorithms
Classic onsite implementation topic; no strong or weak signal, so maintain normal coverage.

What's being tested
This tests the LRU cache pattern: combining a hash map with a doubly linked list to support get and put in O(1) time. You must preserve both key-value lookup and recency ordering while correctly handling updates, eviction, capacity limits, and empty states.
Patterns & templates
-
Hash map + doubly linked list — map keys to nodes; list order is least-recently-used to most-recently-used.
-
Use sentinel
headandtailnodes to eliminate boundary branches when removing or inserting list nodes. -
get(key)returns-1or the agreed absence value; on a hit, detach the node and move it to the MRU end. -
put(key, value)updates existing nodes without increasing size; otherwise insert at MRU, then evict the LRU node if over capacity. -
Factor helpers such as
remove(node)andinsert_at_front(node); each performs constant pointer updates and keeps invariants centralized. -
Target
O(1)average time for both operations andO(capacity)space; never scan the list to find keys. -
Clarify capacity
0, repeated keys, repeated reads, missing keys, and whether values may benullor mutable objects.
Common pitfalls
Pitfall: Updating a key without moving it to MRU makes eviction order incorrect; every successful access must refresh recency.
Pitfall: Removing the LRU node from the list but not the map leaves stale entries and breaks size accounting.
Pitfall: Claiming
O(1)while using an array, singly linked list, or linear search for eviction violates the required complexity.
Practice these
The practice cards below cover the canonical variants — solve all of them and time yourself.
Practice questions
Data Manipulation (SQL/Python)
You viewed coding/data material, but have no solved signal; keep moderate practice on event ordering and windows.
What's being tested
These problems test event-stream processing: filtering records by type and field validity, maintaining global state with set, and propagating information across events in order. You must also demonstrate stable sorting, identifier-based deduplication, timestamp windowing, and the ability to analyze time and space complexity.
Patterns & templates
-
Use a global
setfor distinct nonempty PII values; membership and insertion are amortizedO(1), withO(U)space forUunique values. -
Filter early by event type and recognized fields, then normalize or validate values before adding them to state.
-
Process events chronologically with
sorted(events, key=lambda e: e["timestamp"]); preserve input order for equal timestamps when required. -
For deduplication by identifier, use a
setof seen IDs or a dictionary retaining the first/last record, depending on requirements. -
Implement time windows with
timestamp >= start and timestamp <= end; explicitly confirm inclusive versus exclusive boundaries. -
Fraud propagation requires checking suspicious PII before mutating state; classify the current event first, then add its values.
-
A one-pass scan after sorting is typically
O(n log n)time andO(n + U)space; avoid nested comparisons that becomeO(n²).
Common pitfalls
Pitfall: Counting repeated values, empty strings,
None, or unrecognized fields instead of filtering them before set insertion.
Pitfall: Adding an underwriting event’s PII before checking it, which incorrectly makes the event suspicious based on its own newly introduced value.
Pitfall: Sorting without a deterministic tie-breaker or silently choosing the wrong timestamp-window inclusivity.
Practice these
The practice cards below cover the canonical variants — solve all of them and time yourself.
Practice questions
Software Engineering Fundamentals
No weakness signal, but Affirm coding favors clean, evolving requirements; keep this as a steady production-coding review.

What's being tested
These problems test whether you can turn ambiguous game rules into a precise, stateful model with explicit invariants, termination behavior, and deterministic outcomes. The interviewer is probing data structures, simulation design, algorithmic complexity, object-oriented boundaries, and your ability to generalize from two players to players without duplicating logic. At Affirm, this reflects production engineering fundamentals: correctness under edge cases, reproducible behavior for debugging, and APIs whose rules remain extensible as requirements change.
Core knowledge
-
Domain modeling should separate immutable
Cardvalues (rank,suit), player state, the deck or card ownership, and the rules engine. Avoid embedding game rules insidePlayer; aGameorchestrator plus replaceableRuleSetkeeps extensions testable. -
Represent ownership explicitly: each card is in exactly one location—player hand, active round, persistent pot, or discarded/exhausted state. An invariant such as
sum(player.cards) + pot.size + deck.size == 52catches duplicate-card and lost-card bugs. -
A round-based simulation usually needs
deal(),playRound(),resolveTie(),awardPot(), andisTerminated(). Keep transitions atomic: determine eligible cards, compare ranks, move all played cards into the pot, then award the complete pot to winners. -
For a two-player high-card game, a queue or deque gives amortized removal from the front and insertion at the back. A complete simulation is rounds and space for cards; avoid repeatedly shifting an array.
-
For players, compare each active player's top card in one pass: work per round and temporary space. Uneven round-robin dealing requires an explicit policy—typically player receives the next card while cards remain—rather than assuming equal hand sizes.
-
Tie resolution must define whether all tied players share the pot, continue competing, or trigger a persistent-pot rule. For a persistent pot, add every played card to the pot before the next round; the eventual winner receives
pot + current round cards, preventing tied cards from disappearing. -
Define exhaustion semantics before coding. If a player has no card when a round begins, they may be eliminated, lose immediately, or cause a declared invalid state; selecting one policy is more important than choosing a particular implementation.
-
A deterministic shuffle requires an injectable seeded pseudorandom generator, such as
random.Random(seed), rather than a global RNG. Record the seed and initial deck ordering so a failing game can be replayed exactly in tests and debugging. -
Termination must be a formal predicate, not “the loop will eventually finish.” Typical rules are “one player owns every card,” “only one non-empty player remains,” or “all cards are exhausted”; add a maximum-round guard to detect malformed rules or cycles.
-
For winner reporting, distinguish cards won during play from cards currently held. If the requested result is final ownership, count the winner’s final collection; if it is round victories, maintain a separate score field rather than inferring it from card count.
-
An optimal-play game differs from random simulation: define legal moves, terminal utilities, and whose turn it is, then use minimax for small finite state spaces. Memoization reduces repeated states, while alpha-beta pruning can reduce explored nodes without changing the result.
-
Use property-based tests and invariant checks: every original card appears exactly once, a deterministic seed reproduces the same result, pot size changes consistently, and every completed game satisfies its termination predicate. Test empty decks, all-card ties, duplicate ranks, one player, and .
Worked example — Design a High-Card Game with a Persistent Tie Pot
For “Design a High-Card Game with a Persistent Tie Pot,” I would first clarify whether a tie means all tied players continue, whether non-tied players remain eligible, who receives the pot, and what happens when a player cannot contribute a card. I would declare a concrete default: each active player plays one card, all played cards enter a persistent pot, the highest unique rank wins the pot, and an all-way tie carries forward. I would organize the answer around four pillars: immutable card/deck modeling, player-owned queues, a round-transition state machine, and termination plus invariant validation. I would make ownership explicit so a played card moves from a player queue to the pot and then from the pot to the winner, never existing in two collections. The key tradeoff is whether to physically transfer card objects or track only counts and scores; physical transfer costs little for 52 cards but makes correctness and future rule changes much clearer. I would inject the shuffle RNG and expose the seed so two identical inputs produce identical games. I would close by saying that, with more time, I would add property-based tests, a replay log of state transitions, and a policy interface for exhaustion and tie resolution.
A second angle — Determine winner in optimal-play card game
“Determine winner in optimal-play card game” shifts from simulation to adversarial search: the engine must choose the best legal move rather than consume a predetermined shuffled sequence. I would define the state as the players’ remaining cards, current turn, and any accumulated pot, then define a terminal utility such as win/loss or final score. For small decks, recursive minimax with memoization is straightforward; encode the state canonically so equivalent positions share a cache entry. If the state space grows, alpha-beta pruning, symmetry reduction, or dynamic programming may be necessary, and I would state the practical size limit rather than claiming minimax scales indefinitely. The same emphasis on explicit rules, ownership, terminal conditions, and reproducibility still applies.
Common pitfalls
Pitfall: Treating a tie as merely “no winner this round” and dropping the played cards loses the persistent-pot rule. Move every played card into a pot before deciding whether the pot is awarded or carried forward.
A tempting implementation mutates player arrays while iterating over them, then accidentally skips players or miscounts cards. Prefer a snapshot of eligible players for the round and perform ownership transfers in a clear, ordered phase.
Pitfall: Saying “the game ends when someone wins” is not a sufficient termination rule. State the exact predicate, handle empty players and all-way ties, and include a defensive round limit or cycle detector for invalid configurations.
Connections
The interviewer may pivot to state machines, invariant-based testing, dependency injection for deterministic randomness, or minimax and memoization. They may also ask how the design changes for event sourcing, replayable debugging, or concurrent game sessions.
Practice questions
Behavioral & Leadership
You viewed behavioral content five times; keep polishing concise ownership and cross-functional impact stories.
What's being tested
These conversations test ownership: taking an ambiguous engineering problem from discovery through implementation, rollout, and measurable outcome rather than stopping when the code merges. They probe whether you can make sound technical tradeoffs, communicate constraints across teams, and create momentum when dependencies, deadlines, or priorities conflict. Affirm cares because financial products require reliable, auditable systems where engineers must coordinate effectively while protecting correctness, customer trust, and operational stability. The interviewer is looking for concrete evidence, not a claim that you are “a leader”: what you personally decided, how you influenced others, and what changed afterward.
Core knowledge
-
STAR-L structures behavioral answers: Situation, Task, Actions, Result, and Learning. Spend most time on your actions, distinguish “I” from “we,” and quantify engineering outcomes such as latency, error rate, deployment frequency, or incident reduction.
-
Ownership boundaries should be explicit: state what you directly controlled, what another team owned, and how you resolved the interface. Strong engineers do not claim credit for another team’s work; they explain how they made the overall system succeed.
-
For a high-impact project, connect the technical problem to a measurable engineering outcome: reducing
p99latency, improving availability, eliminating duplicate writes, shortening recovery time, or lowering operational toil. Avoid vague claims such as “the system was better.” -
Use risk decomposition before proposing work: identify correctness, availability, security, privacy, performance, migration, and operational risks. For a payment-related flow, a small increase in throughput is not worth weakening authorization correctness or introducing unreconciled state.
-
Explain tradeoffs with constraints, not preferences. For example, choosing synchronous validation gives simpler correctness semantics but increases tail latency; asynchronous processing improves responsiveness but requires retries, durable state, idempotency, and user-visible status handling.
-
Idempotency makes retries safe: persist an idempotency key with the operation result and enforce a uniqueness constraint in
Postgres. Handle timeout-after-commit separately from failure-before-commit; the client must be able to retry without creating a second financial effect. -
Incremental rollout reduces blast radius. Use feature flags, shadow traffic, canaries, or percentage-based deployment; define rollback triggers using
p95/p99latency, error rate, duplicate-operation rate, and support or reconciliation signals. -
Cross-functional leadership for an SWE means translating interfaces and risks for partner engineers, QA, SRE, security, or operations. Bring a concrete proposal, document API and ownership contracts, surface blockers early, and make disagreement resolution evidence-based.
-
Observability is part of delivery, not post-launch decoration. Instrument structured logs, traces, counters, and dashboards; define the expected invariant and alert threshold before rollout so “success” is testable rather than anecdotal.
-
When deadlines conflict with quality, separate must-have safety properties from deferrable scope. A staged implementation can ship a narrow, well-tested path while explicitly postponing automation or optimization; it should not silently defer authentication, data integrity, rollback, or auditability.
-
Demonstrate technical influence without authority through design reviews, small prototypes, benchmarks, RFCs, and tests. A useful example explains how new evidence changed the plan, rather than presenting persuasion as winning an argument.
-
Close with learning and prevention: describe a design change, test, runbook, alert, or review practice that prevents recurrence. If the result was mixed, acknowledge it and explain how you would measure or improve the next iteration.
Worked example: Describe your highest-impact project
In the first 30 seconds, I would clarify the project’s scope, my role, the affected system, the baseline, and how impact was measured—for example, whether “impact” means lower p99 latency, fewer failed transactions, or reduced operational load. I would state the constraints I inherited, such as a fixed launch date, compatibility with an existing API, or a requirement not to interrupt in-flight requests. I would organize the answer around four pillars: diagnosing the bottleneck, selecting the design, coordinating dependent teams, and validating the rollout. For the technical story, I might explain how I replaced an unsafe retry path with idempotency keys and a durable operation record, while preserving the existing client contract. I would explicitly flag the tradeoff: synchronous confirmation simplified user-visible semantics, but increased tail latency, so I chose asynchronous status polling for non-critical work and retained synchronous handling where immediate authorization was required. I would describe my concrete collaboration actions—writing the interface contract, reviewing failure modes with the owning team, adding dashboards, and agreeing on rollback criteria. I would quantify the result with before-and-after measurements, such as duplicate operations falling to zero in sampled traffic and p99 latency improving without an availability regression. I would close by saying that with more time, I would automate the rollout analysis and add a failure-injection test covering timeout-after-commit behavior.
A second angle: Hiring Manager Behavioral Interview: Challenging Project, Deadlines, Cross-Team Work
Here the emphasis shifts from the project’s headline impact to how you operate under pressure and across team boundaries. Frame the deadline, the non-negotiable correctness constraints, and the decision process used to reduce scope without creating hidden reliability debt. Explain how you found the system improvement opportunity—through traces, incident data, code inspection, or customer-facing failure reports—without drifting into product strategy. Highlight a moment of disagreement and show the artifact or experiment that resolved it. End with how you preserved trust: transparent status, documented ownership, and a rollout plan that let partner teams verify their assumptions.
Common pitfalls
Pitfall: Giving an analytical result without an engineering baseline. Saying “the project increased reliability” is weak unless you define the failure mode, measurement window, comparison point, and whether the metric could have changed because of unrelated traffic or deployment differences.
Pitfall: Describing leadership as directing people or attending meetings. “I kept everyone aligned” does not establish ownership; explain the design decision, dependency you unblocked, risk you surfaced, or mechanism you introduced to make collaboration effective.
Pitfall: Going too shallow technically. A polished story that omits retries, partial failure, rollback, data integrity, or observability can sound like project coordination rather than software engineering. Include one concrete failure mode and why your design handles it.
Connections
The interviewer may pivot from ownership to incident response, distributed-systems failure handling, API and schema evolution, or operational excellence. Be ready to connect the behavioral story to code reviews, testing strategy, on-call decisions, and safe deployment practices.
Practice questions