All articles
ProductJul 29, 2026 · 8 min

Designing a Returns Data Model That Scales

DA
Defne Aksoy
Head of Product

Most merchants build their returns data model by accident. A support agent needs a field to log why a customer sent something back, so someone adds a free-text column. Finance needs to track refund amounts, so a spreadsheet gets bolted on. Eighteen months later, nobody can answer a simple question like 'what is our return rate for size-related reasons on dresses over $80' without a week of manual data wrangling. The problem is never the reporting tool — it is that the underlying returns-api-integration-guide was never designed as a real entity model. If you get the schema wrong at the RMA (Return Merchandise Authorization) level, every downstream dashboard, forecast, and vendor chargeback claim inherits that weakness.

This matters more than it used to. Return rates for online purchases regularly run in the 20-30% range for apparel, and the operational cost of processing each one is real money — analysts at the National Retail Federation have repeatedly flagged returns as a multi-hundred-billion-dollar drag on US retail alone. A retailer processing tens of thousands of returns a month cannot afford a data model that treats every return as an isolated ticket. It needs a model that composes, so that a single RMA record produces a queryable trail from 'customer clicked initiate return' through 'refund issued' and 'item resold or written off.'

The core entities: RMA, line item, reason, disposition

A scalable returns data model separates four concerns that most homegrown systems collapse into one table. Keeping them distinct is what lets analytics and finance query independently without joining across a mess of nullable columns.

  • RMA (header): one record per return authorization — order reference, customer, channel, status, created/closed timestamps, total refund amount.
  • Line item: one record per SKU/quantity being returned, linked to the original order line, not just the order — this is what lets you compute per-product return rates.
  • Reason: a normalized taxonomy applied at the line-item level (not the RMA level), because a single return can bundle a wrong-size item and a changed-mind item in the same box.
  • Disposition: what happens to the physical unit after inspection — restock, liquidate, discard, return-to-vendor, refurbish — captured as its own event, separate from the refund decision.

The line-item-versus-RMA distinction is where most systems fail first. If reason codes live on the RMA header, you lose the ability to say which specific product drove the return, which makes product-level analytics impossible. This is the single biggest reason we push merchants toward a strict return-reason-taxonomy-in-practice applied at the line level from day one, even if the UI only shows customers one reason picker.

Why disposition needs to be its own table, not a status field

Disposition is frequently modeled as just another value in the RMA's status enum — 'restocked', 'refunded', 'closed' all mixed together. This breaks the moment a single RMA contains three units: one restocked, one liquidated because of damage, one sent back to the vendor under a defective-goods agreement. Disposition should be a child event tied to the line item (or even the individual unit, for serialized or high-value goods), with its own timestamp, actor, and warehouse location. That separation is also what makes vendor chargeback recovery and shrinkage analysis possible later — you cannot claim a manufacturing-defect chargeback from a supplier if your data model never captured which specific units were defective versus simply undesired.

If your reason codes and disposition outcomes aren't attached to the line item, your return rate is a headline number — not an operating lever.

A reference schema

The table below is a simplified version of the schema ResReturn uses internally, adapted for platforms like Shopify, Ticimax, and ikas. It is deliberately normalized — you can denormalize into a reporting warehouse later, but the transactional model should stay strict.

EntityKey fieldsCardinalityFeeds
rmaid, order_id, customer_id, channel, status, created_at, closed_at1 per return requestOperations dashboards, SLA timers
rma_line_itemid, rma_id, order_line_id, sku, qty, condition_reportedN per rmaProduct-level return-rate analytics
return_reasonid, line_item_id, reason_code, reason_category, free_text1 per line_item (usually)Reason taxonomy reporting
dispositionid, line_item_id, outcome, warehouse_id, actor_id, decided_at1+ per line_itemVendor chargebacks, shrinkage, resale
refund_eventid, rma_id, amount, method, processor_ref, issued_at1+ per rmaFinance reconciliation

Designing for the queries you'll actually run

A useful test for any proposed schema: write out the five reports your CFO, your merchandiser, and your ops lead will ask for in the first quarter, and check whether each is a single join away. In practice these are almost always the same five:

  1. 1Return rate by SKU and category, over a rolling window
  2. 2Reason mix by product, to catch a sizing or quality issue early
  3. 3Refund cycle time from RMA creation to funds released
  4. 4Disposition outcomes (restock vs. write-off) by vendor, for chargeback recovery
  5. 5Repeat-returner behavior at the customer level, for fraud and policy tuning

Every one of those reports requires reason and disposition to sit below the line-item level, not the RMA header. Data engineering teams generally agree that pushing granularity down to the smallest meaningful unit early is cheaper than retrofitting it later — a point echoed across data-engineering best-practice guidance from teams like McKinsey's digital practice on analytics-ready operational data. Retrofitting a granular model onto years of RMA-level-only history means either an expensive backfill project or permanently degraded historical reporting.

Common modeling mistakes

We see the same handful of mistakes across merchants migrating from spreadsheets or a bolted-on helpdesk field to a real system.

  • Storing reason as free text only, with no normalized code, making cross-product comparison impossible without NLP cleanup.
  • Treating refund and disposition as the same event, which breaks the moment a return is refunded before the item physically arrives (common with 'instant refund' policies).
  • No link back to the original order line — only the order — so you can't tell which of five identical-looking SKUs in one order was actually returned.
  • Mutable status fields instead of an append-only event log, which destroys your ability to measure SLA timing or audit disputed refunds.
  • No warehouse/location dimension on disposition, so restock-vs-write-off decisions can't be tied to a specific facility's performance.

Fixing the reason-code problem alone tends to have outsized payoff — see our deep dive on rma-data-quality for the specific cleanup steps we run with merchants in their first 30 days on ResReturn. Getting reason and disposition right early is also what unlocks reliable return-rate forecasting, since forecasting models are only as good as the categorical granularity of the historical data feeding them.

Migrating an existing system without downtime

Most merchants don't get to design this from scratch — they're migrating off a platform-native returns flow or a support-desk hack. The safest path is additive: introduce the new line-item, reason, and disposition tables alongside the legacy RMA header, backfill what you can from historical order and refund data, and dual-write during a transition window before cutting reporting over. Trying to do a single big-bang migration on a live returns queue is how merchants end up with weeks of unreliable SLA reporting right when they need it most — during a peak return season.

Should reason codes be captured from the customer or assigned during inspection?

Both, as separate fields. Capture the customer-stated reason at RMA creation for speed and UX, then let warehouse staff assign an inspection-verified reason on receipt. The two often disagree — a customer says 'wrong size,' but inspection reveals visible wear — and that gap is itself valuable fraud and quality signal.

How granular should the reason taxonomy be?

Start with 8-12 top-level categories (size, quality defect, not as described, changed mind, wrong item shipped, damaged in transit, late arrival, other) and allow one level of sub-reason underneath the highest-volume categories. Too many top-level codes fragments your reporting; too few hides real signal.

Can this model work across multiple sales channels like Shopify, Ticimax, and ikas at once?

Yes, as long as the RMA header carries a channel field and the line item references a platform-agnostic product identifier rather than a channel-specific SKU string. This is exactly the abstraction ResReturn's returns API maintains so merchants running Shopify and Ticimax storefronts side by side get one unified reporting layer.

Where should disposition data live if we outsource warehousing to a 3PL?

The disposition event should still be written into your core returns data model, populated via webhook or API from the 3PL's system rather than left siloed in their portal. Otherwise you lose the ability to tie vendor chargebacks and shrinkage analysis back to your own product and reason data.

See it on your own returns.

Start free