Low Level Design: Returns Processing System

Returns Processing System: Low Level Design

Core Data Model

Two primary tables capture return requests and the individual items within them:

-- Returns table
CREATE TABLE returns (
    id            BIGSERIAL PRIMARY KEY,
    order_id      BIGINT NOT NULL,
    user_id       BIGINT NOT NULL,
    status        VARCHAR(20) NOT NULL DEFAULT 'requested',
                  -- requested | approved | label_sent | received | inspecting | resolved
    reason_code   VARCHAR(30) NOT NULL,
                  -- wrong_item | defective | not_as_described | changed_mind | damaged_in_transit | ...
    resolution    VARCHAR(20),
                  -- refund | exchange | store_credit | rejected
    requested_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Return items table
CREATE TABLE return_items (
    id               BIGSERIAL PRIMARY KEY,
    return_id        BIGINT NOT NULL REFERENCES returns(id),
    order_item_id    BIGINT NOT NULL,
    quantity         INT NOT NULL,
    condition        VARCHAR(20),  -- new | like_new | damaged
    inspection_notes TEXT
);

Return Request Flow

Customer initiates return
  → Eligibility check:
      - Item purchased within 30 days?
      - Item not in exclusion list (digital goods, perishables)?
      - Quantity requested ≤ quantity ordered?
  → Auto-approve if: value < $150 AND reason_code in (wrong_item, defective, damaged_in_transit)
  → Manual review queue if: value ≥ $150 OR reason_code = changed_mind
  → Approved → generate prepaid return shipping label
  → Email label PDF to customer, update status = label_sent

Return Label Generation

On approval, the system calls the carrier API (same carrier used for outbound where possible) to generate a prepaid return label:

CarrierAPI.createReturnLabel({
    from: customer_address,
    to:   warehouse_address,
    service: 'ground',
    reference: return_id
})
→ Store label_url, tracking_number on returns record
→ Email label to customer
→ Update status = 'label_sent'

Warehouse Inspection Workflow

When the return arrives at the warehouse:

  1. Warehouse scans return tracking number → system marks status = received.
  2. Inspector opens return in WMS UI → grades each item condition: new / like_new / damaged.
  3. Inspector adds notes and submits → status = inspecting → system applies resolution policy rules.

Resolution Policy Rules

Policy engine evaluates per return_item:

condition = 'new'       → restock + refund approved
condition = 'like_new'  → restock + refund approved (if within policy window)
condition = 'damaged'   → route to liquidation/disposal
                          refund approved if damage caused by carrier/defect
                          refund rejected if customer-caused damage

reason_code = 'changed_mind' + condition = 'damaged' → resolution = rejected

Final resolution set on returns.resolution:
  refund | exchange | store_credit | rejected

Refund Processing

IF resolution = 'refund':
    PaymentAPI.refund(order.payment_intent_id, refund_amount_cents)
    → Update order status = 'refunded' (or partially_refunded)
    → Write order event log entry
    → Send refund confirmation email

IF resolution = 'exchange':
    OrderService.createOrder(user_id, exchange_items, payment_total=0)
    → New order created with $0 charge
    → Send exchange confirmation email with new order number

IF resolution = 'store_credit':
    WalletService.credit(user_id, amount_cents)
    → Send store credit notification email

Restocking Logic

IF return_item.condition IN ('new', 'like_new'):
    InventoryService.incrementStock(product_id, warehouse_id, quantity)
    InventoryService.releaseReservation(order_item_id)
    -- Item is sellable again

IF return_item.condition = 'damaged':
    LiquidationService.createRecord(return_item_id)
    -- Item routed to liquidation partner or disposed

Fraud Detection

A background job checks return patterns on every new return request:

SELECT COUNT(*) FROM returns
WHERE user_id = ? AND requested_at > NOW() - INTERVAL '90 days'

IF count > 3:
    Flag return for manual review
    Add user_id to returns_review_queue with reason = 'high_return_rate'
    Optionally: restrict auto-approve for this user_id

Fraud signals also include: same item returned multiple times, returns clustered around sale events, mismatch between returned item weight and expected weight (carrier scan data).

Analytics & Merchandising Insights

-- Return rate by product
SELECT product_id,
       COUNT(ri.id)::FLOAT / SUM(oi.quantity) AS return_rate
FROM return_items ri
JOIN order_items oi ON ri.order_item_id = oi.id
GROUP BY product_id
ORDER BY return_rate DESC;

-- Return rate by reason code
SELECT reason_code, COUNT(*) AS returns
FROM returns
WHERE requested_at > NOW() - INTERVAL '30 days'
GROUP BY reason_code
ORDER BY returns DESC;

High return rates on a product surface in the merchandising dashboard as a signal to review product listings, sizing guides, or supplier quality.

Key API Endpoints

POST /returns                     -- customer initiates return
GET  /returns/{id}               -- get return status + items
POST /returns/{id}/approve       -- manual approval (agent)
POST /returns/{id}/inspect       -- warehouse submits inspection grades
GET  /returns?user_id=&status=   -- list returns (admin)
GET  /returns/analytics          -- return rate reports

Key Design Decisions

  • Eligibility and policy rules are externalized to configuration so merchandising can change return windows without a code deploy.
  • Resolution policy is deterministic and auditable — every decision is logged with the rule that triggered it.
  • Refund and exchange flows reuse existing PaymentAPI and OrderService to avoid duplicated logic.
  • Fraud threshold (3 returns / 90 days) is configurable per user segment (e.g., higher threshold for loyalty members).
  • Append-only return event log mirrors the order event pattern for consistent auditability.

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

See also: Shopify Interview Guide

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

Scroll to Top