Low Level Design: Peer-to-Peer Payment Service

Problem Statement

Design a peer-to-peer payment service that lets users send money to one another, link external bank accounts, and manage wallet balances with fraud controls and regulatory limits.

Core Requirements

  • User wallet with stored balance
  • Transfer initiation between users
  • Bank account linking and micro-deposit verification
  • Hold and release flow for pending funds
  • Daily and per-transaction limits enforcement
  • Fraud detection and transaction review

Data Model

users
  id            BIGINT PK
  email         VARCHAR(255) UNIQUE NOT NULL
  phone         VARCHAR(20) UNIQUE
  kyc_status    ENUM('pending','verified','rejected')
  created_at    TIMESTAMP

wallets
  id            BIGINT PK
  user_id       BIGINT FK -> users.id UNIQUE
  balance       DECIMAL(18,2) NOT NULL DEFAULT 0
  held_balance  DECIMAL(18,2) NOT NULL DEFAULT 0
  currency      VARCHAR(3) DEFAULT 'USD'
  updated_at    TIMESTAMP

bank_accounts
  id            BIGINT PK
  user_id       BIGINT FK -> users.id
  account_hash  VARCHAR(64) NOT NULL
  routing_hash  VARCHAR(64) NOT NULL
  status        ENUM('pending','verified','removed')
  verified_at   TIMESTAMP

transfers
  id            BIGINT PK
  sender_id     BIGINT FK -> users.id
  receiver_id   BIGINT FK -> users.id
  amount        DECIMAL(18,2) NOT NULL
  status        ENUM('pending','held','completed','failed','reversed')
  initiated_at  TIMESTAMP
  settled_at    TIMESTAMP
  idempotency_key VARCHAR(64) UNIQUE NOT NULL
  note          VARCHAR(255)

fraud_signals
  id            BIGINT PK
  transfer_id   BIGINT FK -> transfers.id
  signal_type   VARCHAR(64)
  score         FLOAT
  action        ENUM('allow','review','block')
  created_at    TIMESTAMP

API Design

POST   /wallets/{userId}/transfer          Initiate transfer
GET    /wallets/{userId}/balance           Get available and held balance
POST   /bank-accounts                      Link a bank account
POST   /bank-accounts/{id}/verify          Verify via micro-deposits
GET    /transfers/{transferId}             Get transfer status
POST   /transfers/{transferId}/cancel      Cancel a pending transfer
GET    /users/{userId}/transfers           Transfer history with pagination

Transfer Initiation Flow

  1. Validate sender KYC status is verified
  2. Check daily and per-transaction limits against transfer amount
  3. Run fraud scoring — block or flag for manual review if score exceeds threshold
  4. Deduct amount from sender wallet balance and add to held_balance (atomic DB transaction)
  5. Insert transfer row with status held
  6. Publish transfer.held event to queue for async settlement

Hold and Release Flow

Funds sit in held_balance until settlement completes. A background worker picks up transfer.held events and calls the payment network (ACH, card rails, or internal). On success the worker atomically decreases sender held_balance and increases receiver balance, then marks the transfer completed. On failure it returns the amount to sender balance and marks the transfer failed.

BEGIN TRANSACTION;
  UPDATE wallets SET held_balance = held_balance - :amount WHERE user_id = :sender_id;
  UPDATE wallets SET balance = balance + :amount WHERE user_id = :receiver_id;
  UPDATE transfers SET status = 'completed', settled_at = NOW() WHERE id = :transfer_id;
COMMIT;

Bank Account Linking and Verification

The service stores only hashed account and routing numbers. Verification uses two micro-deposits of random cents sent to the external account. The user confirms both amounts within 72 hours. A token-based approach (Plaid, Stripe Financial Connections) can replace micro-deposits for faster verification.

Limits Enforcement

limits
  user_id              BIGINT FK -> users.id
  daily_send_limit     DECIMAL(18,2) DEFAULT 2500.00
  per_tx_limit         DECIMAL(18,2) DEFAULT 1000.00
  daily_sent_today     DECIMAL(18,2) DEFAULT 0
  limit_reset_at       TIMESTAMP

-- On each transfer attempt:
IF transfer.amount > limits.per_tx_limit THEN reject
IF limits.daily_sent_today + transfer.amount > limits.daily_send_limit THEN reject
UPDATE limits SET daily_sent_today = daily_sent_today + :amount WHERE user_id = :sender_id

Limits reset daily via a scheduled job or lazily when limit_reset_at is in the past. Use a Redis counter for high-throughput limit checks with a database fallback.

Fraud Detection

A fraud scoring service evaluates each transfer synchronously before funds are moved. Signals include: velocity (many transfers in short window), device fingerprint mismatch, new receiver, unusual amount, and geographic anomaly. Scores below a threshold allow; scores above block; middle range queues for manual review. Suspicious transfers hold funds until a human agent approves or rejects.

Idempotency

Clients send a unique idempotency_key with each transfer request. The server stores it in the transfers table with a UNIQUE constraint. Duplicate requests with the same key return the existing transfer record rather than creating a new debit.

Scaling Considerations

  • Partition transfers table by initiated_at date for archival and query performance
  • Use optimistic locking or SELECT FOR UPDATE on wallet rows to prevent race conditions
  • Async settlement workers scale independently from the API layer
  • Separate read replicas for balance queries and transaction history
  • Rate-limit transfer initiation per user at the API gateway

Interview Tips

  • Start with the atomic hold-and-release flow — interviewers probe for double-spend awareness
  • Clarify whether wallet-to-wallet is instant (internal ledger) or requires external rails (ACH = T+1)
  • Discuss idempotency keys before the interviewer asks — it signals production experience
  • Mention regulatory constraints: KYC/AML, reporting thresholds (USD 10k CTR), PCI scope if cards are involved
What is a peer-to-peer payment service in system design?

A peer-to-peer (P2P) payment service allows individuals to transfer money directly to one another through a digital platform without routing through a traditional bank branch. In system design, it involves a ledger of user accounts and balances, an orchestration layer that debits the sender and credits the receiver atomically, and integrations with external payment rails (ACH, card networks, or crypto) for funding and withdrawal. Key design goals include strong consistency for balance updates, idempotent transaction handling, and real-time fraud screening.

How does a hold-and-release payment flow work?

In a hold-and-release flow the system first places a temporary hold (authorization) on the sender’s funds, reducing their available balance without yet crediting the receiver. Once all validations pass — fraud checks, KYC limits, and external network confirmation — the hold is captured and the receiver’s balance is credited. If any step fails the hold is voided and the sender’s full balance is restored. This two-phase approach prevents double-spending while keeping funds recoverable during async settlement delays.

How do you ensure idempotency in payment transactions?

Idempotency in payments is achieved by associating each transaction request with a client-generated idempotency key (a UUID). The server stores the key and its outcome in a durable store before executing the transfer. On any retry — caused by network timeouts or client crashes — the server looks up the key and returns the previously recorded result without re-executing the transfer. The idempotency record should be written in the same database transaction as the balance update, and expired keys are purged after a safe retention window (typically 24–48 hours).

How do daily transfer limits and fraud checks integrate into the payment flow?

Daily transfer limits are enforced by querying a rolling-window counter (stored in Redis or a time-series table) that tracks the total amount sent by a user in the past 24 hours. If the requested transfer would exceed the limit the request is rejected before any funds are moved. Fraud checks run in parallel as a synchronous call to a risk-scoring service that evaluates signals such as device fingerprint, velocity patterns, and recipient history. A high risk score either blocks the transaction outright or routes it to a manual review queue. Both checks sit between request validation and the hold step so that no funds are ever touched for a blocked transaction.

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

See also: Coinbase Interview Guide

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

Scroll to Top