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:
- Warehouse scans return tracking number → system marks
status = received. - Inspector opens return in WMS UI → grades each item condition:
new/like_new/damaged. - 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.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What eligibility checks are required before approving a return?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The system checks that the item was purchased within the return window (typically 30 days), the item is not in an exclusion list (digital goods, perishables, etc.), and the quantity requested does not exceed the quantity originally ordered.”
}
},
{
“@type”: “Question”,
“name”: “How does warehouse inspection determine a return's resolution?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A warehouse inspector grades each returned item as new, like_new, or damaged. A policy rules engine then decides the resolution: new or like_new items are restocked and a refund or exchange is issued; damaged items go to liquidation and refunds may be rejected if damage was customer-caused.”
}
},
{
“@type”: “Question”,
“name”: “How does a returns system detect return fraud?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A background job counts returns per user over a rolling 90-day window. Users exceeding three returns are flagged for manual review and may have auto-approve disabled. Additional signals include repeated returns of the same item, returns clustered around sale events, and weight mismatches from carrier scan data.”
}
},
{
“@type”: “Question”,
“name”: “How does restocking work after a return is inspected?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “If the returned item's condition is new or like_new, the inventory count for that product is incremented and any reservation is released, making it available for sale. Damaged items are routed to a liquidation partner or disposed of rather than returned to sellable inventory.”
}
}
]
}
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