# Returns Portal System Low-Level Design

Source: https://www.techinterview.org/post/3233467804/lld-returns-portal/
Updated: 2026-04-17 · techinterview.org

## What is a Returns Portal?

A returns portal allows customers to self-service returns and exchanges: select items to return, choose a reason, get a prepaid shipping label, and track the return status. Amazon, Shopify merchants, and Zappos are known for seamless return experiences that drive customer loyalty. The system must handle the full lifecycle: return request to label generation to item receipt to inspection to refund or exchange processing. Returns typically represent 15-30% of e-commerce orders.

## Requirements

  - Customer initiates return on eligible orders (within return window, e.g., 30 days)

  - Return methods: prepaid label (mail), drop-off at retail location

  - Reasons: defective, not as described, changed mind, wrong size

  - Return outcome: refund to original payment, store credit, or exchange

  - Warehouse receives and inspects returned item; triggers refund on approval

  - Track return status: label created, shipped, received, inspected, refunded

## Data Model


```
ReturnRequest(
    return_id       UUID PRIMARY KEY,
    order_id        UUID NOT NULL,
    customer_id     UUID NOT NULL,
    status          ENUM(REQUESTED, LABEL_SENT, SHIPPED, RECEIVED, INSPECTING,
                         APPROVED, REFUNDED, REJECTED, EXCHANGED),
    items           JSONB,          -- [{order_item_id, quantity, reason, outcome}]
    return_method   ENUM(PREPAID_LABEL, DROP_OFF),
    label_url       VARCHAR,        -- presigned URL for shipping label PDF
    tracking_number VARCHAR,
    carrier         VARCHAR,
    received_at     TIMESTAMPTZ,
    inspection_notes TEXT,
    refund_amount   DECIMAL(10,2),
    refund_method   ENUM(ORIGINAL_PAYMENT, STORE_CREDIT),
    refunded_at     TIMESTAMPTZ,
    created_at      TIMESTAMPTZ
)

ReturnEligibilityRule(
    rule_id         UUID PRIMARY KEY,
    product_category VARCHAR,
    return_window_days INT,
    restockable     BOOL,
    exceptions      JSONB
)
```


## Eligibility Check


```
def check_return_eligibility(order_id, item_ids):
    order = db.get_order(order_id)
    items = [i for i in order.items if i.id in item_ids]
    ineligible = []

    for item in items:
        rule = get_rule(item.product_category)
        days_since_order = (now() - order.placed_at).days

        if days_since_order > rule.return_window_days:
            ineligible.append({'item': item.id,
                'reason': f'Return window of {rule.return_window_days} days has passed'})
            continue

        if item.metadata.get('final_sale'):
            ineligible.append({'item': item.id,
                'reason': 'Item is marked final sale'})
            continue

    return {'eligible': [i for i in items if i.id not in
                         [x['item'] for x in ineligible]],
            'ineligible': ineligible}
```


## Label Generation and Refund Flow


```
def create_return(order_id, item_ids, reasons, outcome, method):
    eligibility = check_return_eligibility(order_id, item_ids)
    if eligibility['ineligible']:
        raise IneligibleItems(eligibility['ineligible'])

    return_request = db.create(ReturnRequest(
        order_id=order_id, status='REQUESTED',
        items=[{'order_item_id': id, 'reason': reasons[id], 'outcome': outcome}
               for id in item_ids],
        return_method=method
    ))

    if method == 'PREPAID_LABEL':
        label = carrier_api.create_return_label(
            from_address=get_order_shipping_address(order_id),
            to_address=warehouse.address,
            package=estimate_package(item_ids)
        )
        db.update(return_request.id, status='LABEL_SENT',
                  label_url=label.url, tracking_number=label.tracking_number)

    return return_request

def process_inspection(return_id, condition, inspector_notes):
    return_req = db.get(return_id)
    approved = condition in ('new', 'like_new', 'good')

    if approved:
        order = db.get_order(return_req.order_id)
        refund_amount = calculate_refund(return_req.items, order)
        payment_service.refund(order.payment_id, refund_amount)
        if condition == 'new':
            inventory_service.restock(return_req.items)
        db.update(return_id, status='REFUNDED',
                  refund_amount=refund_amount, refunded_at=now())
    else:
        db.update(return_id, status='REJECTED', inspection_notes=inspector_notes)
```


## Key Design Decisions

  - Items as JSONB on ReturnRequest — a return covers multiple items; avoids a join table for a simple list

  - Eligibility rules table — configurable per product category without code deploys

  - State machine for return lifecycle — prevents invalid transitions (cannot refund before receiving)

  - Carrier API for prepaid labels — same abstraction as outbound shipping; EasyPost supports return labels

  - Decouple inspection from refund — warehouse staff inspect; refund triggers automatically on approval

Returns portal and e-commerce return system design is discussed in [Amazon system design interview questions](https://www.techinterview.org/post/3233460275/scale-ai-interview-guide-2026-data-infrastructure-rlhf-pipelines-and-ml-engineering/).

Returns portal and refund system design is covered in [Shopify system design interview preparation](https://www.techinterview.org/post/3233459924/shopify-interview/).

Returns processing and refund system design is discussed in [Stripe system design interview guide](https://www.techinterview.org/post/3233460268/stripe-interview-guide-2026-process-bug-bash-round-and-payment-systems/).
