Returns Portal System Low-Level Design

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

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you determine if an item is eligible for return?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Check three conditions: (1) Return window — calculate days since order placed; compare against the product category rule (e.g., 30 days for apparel, 15 days for electronics). (2) Final sale flag — check the order item metadata; final sale items are never returnable. (3) Already returned — check for an existing ReturnRequest containing this order_item_id. Store return eligibility rules in a database table (not hardcoded) so they can be updated per product category without code deploys.”}},{“@type”:”Question”,”name”:”How does prepaid return label generation work?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Call a carrier API (EasyPost, ShipStation) to create a return shipment: from_address = customer shipping address (from the original order), to_address = warehouse. The API returns a label PDF URL and tracking number. Store both on the ReturnRequest record and set status=LABEL_SENT. Email the customer a link to print the label. The carrier API handles carrier selection (cheapest option) and rate calculation transparently.”}},{“@type”:”Question”,”name”:”How do you process a refund after inspection?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”When a warehouse worker marks the inspection complete with a condition (new, like_new, good, damaged), the system automatically triggers the refund if approved. Retrieve the original payment method from the order, calculate the refund amount (full price for full returns, partial for partial returns or restocking fees), and call the payment service to issue a refund to the original payment instrument. Record the refund_amount and refunded_at on the ReturnRequest. Send a confirmation email to the customer.”}},{“@type”:”Question”,”name”:”What happens to returned inventory after inspection?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Based on the inspection condition: "new" condition → restock to inventory (increment quantity_on_hand in the warehouse for that SKU). "like_new" or "good" → may restock as open-box at a discounted price, or send to a liquidation channel. "damaged" → write off (do not restock); record a ADJUST inventory transaction with reason=return_damage. "defective" → route to vendor return process (contact supplier for credit). The disposition decision can be manual or rule-based per product category.”}},{“@type”:”Question”,”name”:”How do you track a return shipment in transit?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use carrier tracking webhooks (same mechanism as outbound shipment tracking). The carrier sends webhook events to your endpoint as the return package moves: picked_up, in_transit, out_for_delivery, delivered. On the "delivered" event: set ReturnRequest.status=RECEIVED and ReturnRequest.received_at=now(). Create a work queue item for the warehouse team to inspect the returned package. Notify the customer that their return was received and is being inspected.”}}]}

Returns portal and e-commerce return system design is discussed in Amazon system design interview questions.

Returns portal and refund system design is covered in Shopify system design interview preparation.

Returns processing and refund system design is discussed in Stripe system design interview guide.

Scroll to Top