<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Saloni Jain</title>
    <description>The latest articles on DEV Community by Saloni Jain (@saloni_jain_811f1b2d26c8d).</description>
    <link>https://dev.to/saloni_jain_811f1b2d26c8d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4027306%2Fe2c16f8a-6f32-4856-bdd4-209342e01844.png</url>
      <title>DEV Community: Saloni Jain</title>
      <link>https://dev.to/saloni_jain_811f1b2d26c8d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saloni_jain_811f1b2d26c8d"/>
    <language>en</language>
    <item>
      <title>The Double-Sell Problem: Keeping One Item in Sync Across Two Marketplaces</title>
      <dc:creator>Saloni Jain</dc:creator>
      <pubDate>Wed, 04 Nov 2026 16:27:59 +0000</pubDate>
      <link>https://dev.to/saloni_jain_811f1b2d26c8d/the-double-sell-problem-keeping-one-item-in-sync-across-two-marketplaces-461h</link>
      <guid>https://dev.to/saloni_jain_811f1b2d26c8d/the-double-sell-problem-keeping-one-item-in-sync-across-two-marketplaces-461h</guid>
      <description>&lt;p&gt;Ask a seller who lists on more than one marketplace what keeps them up at night, and you'll hear the same answer: &lt;strong&gt;selling the same item twice.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A limited-edition pair of sneakers, the only one the seller has, is listed on eBay and on a second marketplace. Two buyers check out within a minute of each other. One gets the sneakers. The other gets a cancellation, and the seller gets a defect on their account.&lt;/p&gt;

&lt;p&gt;From the outside, the fix looks trivial: when it sells in one place, remove it from the other. In practice, that one sentence hides most of the classic problems of distributed systems: unreliable delivery, ordering, retries, concurrency and drift. This is a walkthrough of why cross-listing breaks, and the patterns that make it reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why sellers cross-list in the first place
&lt;/h2&gt;

&lt;p&gt;Selling on more than one channel has become normal for regular sellers: a large marketplace for reach, plus another marketplace or the seller's own store. This piece is about the plumbing underneath that setup.&lt;/p&gt;

&lt;p&gt;The moment one physical item is visible in two places, you have two systems that both believe they can sell it. Everything else follows from that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: there is no shared source of truth
&lt;/h2&gt;

&lt;p&gt;Each marketplace keeps its own idea of how many units you have. On eBay's &lt;a href="https://developer.ebay.com/api-docs/sell/inventory/overview.html" rel="noopener noreferrer"&gt;Inventory API&lt;/a&gt;, quantity lives on an inventory item identified by a seller-defined SKU, and is referenced again by each offer that turns that item into a live listing. A second marketplace has its own record. Neither knows about the other.&lt;/p&gt;

&lt;p&gt;The first design decision, and the most important one, is to stop treating any marketplace as the source of truth. The seller's own inventory record, keyed by SKU, is the master. Marketplaces are &lt;em&gt;projections&lt;/em&gt; of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The master record. Marketplaces are views of this, not the other way round.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SNK-0193&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// increments on every change&lt;/span&gt;
  &lt;span class="na"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ebay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;listingId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;syncedVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;listingId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;syncedVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// behind: needs a push&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;syncedVersion&lt;/code&gt; field does a lot of work later. It lets you see, at any moment, which channel is out of date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: "it sold" arrives late, twice, or not at all
&lt;/h2&gt;

&lt;p&gt;The fastest way to learn about a sale is a webhook: the marketplace calls your endpoint when an order is created. eBay offers this through its &lt;a href="https://developer.ebay.com/api-docs/commerce/notification/overview.html" rel="noopener noreferrer"&gt;Notification API&lt;/a&gt;, with subscriptions to event topics and public keys to verify that a message really came from eBay. Shopify offers webhooks too, signed with an &lt;code&gt;X-Shopify-Hmac-Sha256&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Webhooks are fast, but they are not a guarantee. Shopify's own &lt;a href="https://shopify.dev/docs/apps/build/webhooks" rel="noopener noreferrer"&gt;webhook documentation&lt;/a&gt; is refreshingly blunt about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delivery "isn't always guaranteed," and your app can miss events through handler failures or downtime;&lt;/li&gt;
&lt;li&gt;Shopify "doesn't guarantee ordering within a topic, or across different topics for the same resource";&lt;/li&gt;
&lt;li&gt;duplicate deliveries happen, which is why each one carries an &lt;code&gt;X-Shopify-Webhook-Id&lt;/code&gt; you can use to ignore repeats.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a sync engine that relies only on webhooks will, eventually, miss a sale. And a missed sale on one channel is exactly how the double-sell happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: verify, deduplicate, then act
&lt;/h2&gt;

&lt;p&gt;Every incoming event goes through the same three steps before it touches inventory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleOrderWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// 1. is it genuine?&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-shopify-webhook-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;               &lt;span class="c1"&gt;// 2. have we processed it?&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applySale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                          &lt;span class="c1"&gt;// 3. act exactly once&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;markSeen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2 is what makes the handler &lt;strong&gt;idempotent&lt;/strong&gt;: processing the same event twice has the same effect as processing it once. It's the same idea Stripe applies to API calls with &lt;a href="https://docs.stripe.com/api/idempotent_requests" rel="noopener noreferrer"&gt;idempotency keys&lt;/a&gt;, so a retried request never creates a second charge.&lt;/p&gt;

&lt;p&gt;In practice, &lt;code&gt;markSeen&lt;/code&gt; and &lt;code&gt;applySale&lt;/code&gt; should commit in the same database transaction. If the process crashes between them, you either lose the sale or apply it twice on retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: decrement first, then delist everywhere else
&lt;/h2&gt;

&lt;p&gt;When a sale is confirmed on one channel, the order of operations matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decrement the master record&lt;/strong&gt; in a single atomic update, guarded by the current version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue a delist (or quantity update) for every other channel&lt;/strong&gt; that still shows the item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push those updates with retries&lt;/strong&gt;, marking each channel's &lt;code&gt;syncedVersion&lt;/code&gt; when it succeeds.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SNK-0193'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- 0 rows updated means someone else got there first.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;available &amp;gt; 0&lt;/code&gt; guard and the version check turn a race into a clean failure. It's the same principle behind flash-sale systems that don't oversell, except here the competing buyers arrive through different companies' systems instead of one. If two sales arrive at once, only one decrement succeeds. The other is detected immediately, so the seller can cancel and refund it at once rather than discovering the problem when they go to pack the item.&lt;/p&gt;

&lt;p&gt;Step 2 is a classic dual-write problem: you need to update your database &lt;em&gt;and&lt;/em&gt; reliably trigger calls to external APIs. Writing the "delist this on channel X" jobs into an outbox table in the same transaction, then publishing them separately, avoids losing them if the process dies mid-way. This is the classic use case for the &lt;strong&gt;transactional outbox pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: reconcile, because webhooks will miss things
&lt;/h2&gt;

&lt;p&gt;Even with perfect handlers, you'll miss events: deploys, outages, expired tokens, a marketplace having a bad day. Shopify's documentation says it directly: your app "shouldn't rely on receiving data from Shopify webhooks," and it recommends reconciliation jobs that periodically fetch data updated since the last run.&lt;/p&gt;

&lt;p&gt;A reconciliation loop is the safety net under everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Runs every few minutes, per channel.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;lastReconciledAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchOrdersUpdatedSince&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;since&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;applySaleIfNew&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// same idempotent path as webhooks&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkDrift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// compare channel quantities to master&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;setLastReconciledAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details make this work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reuse the idempotent sale handler.&lt;/strong&gt; A sale found by reconciliation goes through exactly the same code as a webhook, so it doesn't matter which one sees it first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check drift, not just orders.&lt;/strong&gt; Compare each channel's listed quantity against the master record. If a listing shows 1 and the master says 0, push a correction. That catches the case where a delist call quietly failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pattern 4: close the gap at checkout, where the platform allows it
&lt;/h2&gt;

&lt;p&gt;Even a fast pipeline leaves a window of seconds between a sale on one channel and a delist on the other. For one-of-a-kind items, some platforms let you close that window at checkout instead.&lt;/p&gt;

&lt;p&gt;eBay's Inventory API documentation describes a &lt;strong&gt;Real-time Inventory Check&lt;/strong&gt;, where "eBay will make a call to this endpoint at checkout" to confirm enough quantity exists before the purchase completes. If your master record says the item is gone, the second buyer is stopped before paying, not after.&lt;/p&gt;

&lt;p&gt;Not every marketplace offers a hook like this, but where one exists, it's the strongest protection against a double-sell, because it moves the check from &lt;em&gt;after&lt;/em&gt; the sale to &lt;em&gt;during&lt;/em&gt; it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: the same item isn't the same data
&lt;/h2&gt;

&lt;p&gt;Inventory is only half the job. Getting an item listed on a second channel at all means mapping one data model onto another:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Categories don't line up.&lt;/strong&gt; One marketplace's "Cameras &amp;amp; Photo &amp;gt; Film Cameras" is another's "Electronics &amp;gt; Cameras". You need a category map, and a fallback for items that don't fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attributes differ.&lt;/strong&gt; eBay uses item specifics; other platforms use their own fields, or none. Brand, model, size and condition need mapping, and condition labels rarely match one to one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editing rules differ.&lt;/strong&gt; eBay notes that listings created through its Inventory API can't then be edited in Seller Hub or other listing tools; revisions have to go through the API. If a seller edits by hand somewhere else, your system and the marketplace disagree.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why import tools matter so much to sellers. A marketplace that can take existing listings from another platform does this mapping once, instead of making the seller retype every item. TrueGether, one of the &lt;a href="https://www.truegether.com/eBayAlternative.html" rel="noopener noreferrer"&gt;eBay alternatives&lt;/a&gt; built for independent sellers, imports listings from eBay and Shopify, and the hard part of any importer like that isn't fetching the data. It's deciding what to do with the fields that don't map cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for anyone building cross-channel sync
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ijdc4xlw06ekbsjd2ig.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ijdc4xlw06ekbsjd2ig.jpg" alt="description: &amp;quot;Selling on eBay and an eBay alternative? Why one item can sell twice, and the fixes: idempotent webhooks, atomic stock updates, an outbox and reconciliation.&amp;quot;" width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Own the source of truth.&lt;/strong&gt; Keep a master inventory record keyed by SKU, with a version number. Marketplaces are projections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat every event as untrusted and repeatable.&lt;/strong&gt; Verify signatures, deduplicate by event ID, and make the handler idempotent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decrement atomically.&lt;/strong&gt; Guard updates with &lt;code&gt;available &amp;gt; 0&lt;/code&gt; and a version check so races fail cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't lose side effects.&lt;/strong&gt; Use an outbox for delist and update calls to other channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconcile on a schedule.&lt;/strong&gt; Assume webhooks will be missed, and compare quantities across channels to catch drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use checkout-time checks where they exist.&lt;/strong&gt; They're the only way to stop a double-sell before the buyer pays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map data deliberately.&lt;/strong&gt; Categories, attributes and condition labels need explicit rules and a fallback.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Cross-listing looks like a sales tactic, but it's really a consistency problem: one physical object, several systems that each think they can sell it, and networks that don't promise delivery or order. The sellers who get it right, or the tools they rely on, aren't the ones with the fastest webhooks. They're the ones who assume events will be late, duplicated or missing, and design for that from the start.&lt;/p&gt;

&lt;p&gt;If you're building or choosing a cross-listing setup, test it against the seven-point checklist above before trusting it with a one-of-a-kind item.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why do cross-listed items sometimes sell twice?
&lt;/h3&gt;

&lt;p&gt;Because each marketplace keeps its own stock count, and the "item sold" signal from one channel can arrive late, twice or not at all. Until the other listing is updated, both channels still show the item as available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Are webhooks enough to keep inventory in sync?
&lt;/h3&gt;

&lt;p&gt;No. Webhooks are fast but not guaranteed, and Shopify's documentation recommends reconciliation jobs alongside them. Use webhooks for speed and a scheduled reconciliation for correctness.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does idempotent mean for a webhook handler?
&lt;/h3&gt;

&lt;p&gt;Processing the same event twice has the same effect as processing it once. In practice: deduplicate by event ID and record the event in the same transaction that applies the sale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I move my eBay listings to another marketplace without retyping them?
&lt;/h3&gt;

&lt;p&gt;Often, yes. Some marketplaces can import existing listings directly or accept an inventory file. &lt;a href="https://www.truegether.com/" rel="noopener noreferrer"&gt;TrueGether&lt;/a&gt;, for example, imports listings from eBay and Shopify and accepts inventory file uploads. Whichever platform you use, check the imported categories, item specifics and condition labels before going live, because those are the fields that rarely map one to one.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Platform behaviour described here comes from eBay's Inventory and Notification API documentation and Shopify's webhook documentation, checked October 2026. APIs change, so always confirm against current docs before building.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>architecture</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Moving Marketplace Listing Pages From Microdata to JSON-LD</title>
      <dc:creator>Saloni Jain</dc:creator>
      <pubDate>Fri, 30 Oct 2026 15:21:30 +0000</pubDate>
      <link>https://dev.to/saloni_jain_811f1b2d26c8d/moving-marketplace-listing-pages-from-microdata-to-json-ld-4o53</link>
      <guid>https://dev.to/saloni_jain_811f1b2d26c8d/moving-marketplace-listing-pages-from-microdata-to-json-ld-4o53</guid>
      <description>&lt;p&gt;If your product pages were built a few years ago, there's a good chance their structured data is written in &lt;strong&gt;Microdata&lt;/strong&gt;: &lt;code&gt;itemscope&lt;/code&gt;, &lt;code&gt;itemtype&lt;/code&gt; and &lt;code&gt;itemprop&lt;/code&gt; attributes spread through the HTML. It works, but it's tied to the template. Every time someone redesigns a component, the markup can quietly break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON-LD&lt;/strong&gt; keeps the same information in one &lt;code&gt;&amp;lt;script type="application/ld+json"&amp;gt;&lt;/code&gt; block, separate from the layout. Google supports all three formats (JSON-LD, Microdata and RDFa) and says they're equally fine when the markup is valid, but it &lt;a href="https://www.searchenginejournal.com/google-updates-structured-data-guidance-to-clarify-supported-formats/478544/" rel="noopener noreferrer"&gt;recommends JSON-LD&lt;/a&gt; where your setup allows, as the easiest to implement and maintain at scale and the least prone to errors.&lt;/p&gt;

&lt;p&gt;On a marketplace, where listing pages are generated from seller data you don't fully control, that maintainability matters even more. Here's how we're approaching the move.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Before: what Microdata looks like on a listing page
&lt;/h2&gt;

&lt;p&gt;A typical Microdata listing mixes the markup into the visible HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;itemscope&lt;/span&gt; &lt;span class="na"&gt;itemtype=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.org/Product"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Canon EF 50mm f/1.8 STM Lens&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"image"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/img/listing-123.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Canon EF 50mm lens"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"offers"&lt;/span&gt; &lt;span class="na"&gt;itemscope&lt;/span&gt; &lt;span class="na"&gt;itemtype=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.org/Offer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"priceCurrency"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"USD"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;$&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"price"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"89.00"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;89.00&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"availability"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.org/InStock"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;itemprop=&lt;/span&gt;&lt;span class="s"&gt;"itemCondition"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.org/NewCondition"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's valid, but the structured data now depends on the exact nesting of &lt;code&gt;div&lt;/code&gt;s. Move the price into a different component during a redesign, and &lt;code&gt;price&lt;/code&gt; can end up outside its &lt;code&gt;Offer&lt;/code&gt; without anyone noticing.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. After: the same data as JSON-LD
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Product&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Canon EF 50mm f/1.8 STM Lens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.example.com/img/listing-123.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Canon EF 50mm f/1.8 STM lens, new in box.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Brand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Canon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;offers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Offer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.example.com/listing/123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;89.00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;priceCurrency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;availability&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org/InStock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;itemCondition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org/NewCondition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;seller&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Person&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;example_seller&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page's HTML can now change freely, as long as the JSON-LD still describes what the buyer sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Generate it on the server from your listing model
&lt;/h2&gt;

&lt;p&gt;Hand-writing JSON inside a template is how bugs creep back in. Build it from the same listing object that renders the page, and serialize it with a real JSON library.&lt;/p&gt;

&lt;p&gt;![Five steps to generate JSON-LD for a listing page: listing model, map values, serialize, escape, embed and test]&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzrmheabnc7byjyk4g9bl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzrmheabnc7byjyk4g9bl.jpg" alt="Five steps to generate JSON-LD for a listing page: listing model, map values, serialize, escape, embed and test" width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a Java stack, for example, with Jackson:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LinkedHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Offer"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCanonicalUrl&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"price"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPrice&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setScale&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;RoundingMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;HALF_UP&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;toPlainString&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"priceCurrency"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"USD"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"availability"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isSold&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"https://schema.org/SoldOut"&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://schema.org/InStock"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"itemCondition"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conditionUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getCondition&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
&lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"seller"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Person"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSellerName&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;

&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LinkedHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@context"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://schema.org"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"@type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Product"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTitle&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"image"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getImageUrls&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getShortDescription&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"offers"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;objectMapper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in that snippet are there on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Price as a plain decimal string&lt;/strong&gt;, with no currency symbol or thousands separator, and the currency in &lt;code&gt;priceCurrency&lt;/code&gt; as an ISO code such as &lt;code&gt;USD&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability follows the listing's real state.&lt;/strong&gt; On a marketplace, listings sell out constantly. Marking a sold item &lt;code&gt;InStock&lt;/code&gt; sends buyers to a dead end and breaks the rule that markup must match the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Escape seller-written text before it goes into a script tag
&lt;/h2&gt;

&lt;p&gt;This one is specific to marketplaces and other user-generated content. Listing titles and descriptions are written by sellers. If a title ever contains &lt;code&gt;&amp;lt;/script&amp;gt;&lt;/code&gt;, unescaped JSON-LD can close your script tag early, which breaks the page at best and opens an injection hole at worst.&lt;/p&gt;

&lt;p&gt;A JSON library escapes quotes and backslashes, but it doesn't make text safe inside HTML. Escape &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;amp;&lt;/code&gt; as Unicode escapes after serializing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;safeJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"\\u003c"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;gt;"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"\\u003e"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;amp;"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"\\u0026"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is still valid JSON, and parsers read it exactly as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Map conditions explicitly
&lt;/h2&gt;

&lt;p&gt;Sellers describe condition in their own words; schema.org has a fixed list. Use an explicit mapping rather than passing seller text through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;conditionUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Condition&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;NEW:&lt;/span&gt;         &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"https://schema.org/NewCondition"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;REFURBISHED:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"https://schema.org/RefurbishedCondition"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;USED:&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"https://schema.org/UsedCondition"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// omit rather than guess&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you can't map a value confidently, leave the property out. A wrong condition is worse than a missing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Name the actual seller, and check what you're eligible for
&lt;/h2&gt;

&lt;p&gt;On a marketplace, the person selling the item usually isn't the marketplace itself. Schema.org's &lt;code&gt;Offer&lt;/code&gt; has a &lt;code&gt;seller&lt;/code&gt; property, so the markup can say who the seller is.&lt;/p&gt;

&lt;p&gt;This also matters for what Google can show. Google's &lt;a href="https://developers.google.com/search/docs/advanced/structured-data/merchant-listing" rel="noopener noreferrer"&gt;merchant listing documentation&lt;/a&gt; says the "merchant has to be the seller of the product in order to be eligible for merchant listing experiences." Read that section carefully against your own setup before expecting merchant listing features. Product markup is still useful for describing the page accurately, but don't assume every rich result applies to a marketplace listing.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Remove the Microdata when the JSON-LD goes live
&lt;/h2&gt;

&lt;p&gt;Running both formats for the same product on one page makes it easy for the two to disagree, especially on price and availability. Switch one template at a time, and remove the old &lt;code&gt;itemprop&lt;/code&gt; attributes in the same release.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Validate before and after
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rich Results Test&lt;/strong&gt; on a sample of listing URLs, including sold, used and multi-image listings, not just the tidy ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL Inspection&lt;/strong&gt; in Search Console to see the page as Google renders it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search Console's structured data reports&lt;/strong&gt; after release, to catch errors that only show up across thousands of pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep in mind that valid structured data makes a page &lt;em&gt;eligible&lt;/em&gt; for rich results; it doesn't guarantee them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Generate JSON-LD on the server from the listing model, with a JSON library.&lt;/li&gt;
&lt;li&gt;Price as a plain decimal string, currency as an ISO code.&lt;/li&gt;
&lt;li&gt;Availability and condition reflect the real listing; omit values you can't map.&lt;/li&gt;
&lt;li&gt;Escape &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;amp;&lt;/code&gt; in the serialized JSON.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;seller&lt;/code&gt; for the actual seller, and check merchant listing eligibility rules.&lt;/li&gt;
&lt;li&gt;Remove the old Microdata in the same release.&lt;/li&gt;
&lt;li&gt;Test sold, used and edge-case listings, then watch Search Console.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Moving to JSON-LD isn't a ranking trick. It's a way to describe every listing accurately and keep it accurate as the site changes, which matters more the more listings and sellers you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is JSON-LD better than Microdata for SEO?
&lt;/h3&gt;

&lt;p&gt;Google treats all three formats as equally valid when the markup is correct. It recommends JSON-LD because it's easier to implement and maintain at scale and less prone to errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use JSON-LD and Microdata on the same page?
&lt;/h3&gt;

&lt;p&gt;You can, but describing the same product twice makes it easy for the two to disagree. When migrating, remove the Microdata in the same release that adds the JSON-LD.&lt;/p&gt;

&lt;h3&gt;
  
  
  What should availability be when a listing has sold?
&lt;/h3&gt;

&lt;p&gt;Use a value that matches the page, such as &lt;code&gt;https://schema.org/SoldOut&lt;/code&gt;, rather than leaving &lt;code&gt;InStock&lt;/code&gt; in place.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: written by the TrueGether team. TrueGether is one of the &lt;a href="https://www.truegether.com/eBayAlternative.html" rel="noopener noreferrer"&gt;eBay alternatives&lt;/a&gt; built for independent sellers, with listing pages across categories from electronics to collectibles.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>java</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Rewriting a High-Traffic Page Without Breaking It: Notes from the Listing Details Migration</title>
      <dc:creator>Saloni Jain</dc:creator>
      <pubDate>Thu, 08 Oct 2026 14:47:53 +0000</pubDate>
      <link>https://dev.to/saloni_jain_811f1b2d26c8d/rewriting-a-high-traffic-page-without-breaking-it-notes-from-the-listing-details-migration-1a8g</link>
      <guid>https://dev.to/saloni_jain_811f1b2d26c8d/rewriting-a-high-traffic-page-without-breaking-it-notes-from-the-listing-details-migration-1a8g</guid>
      <description>&lt;p&gt;TrueGether is an &lt;a href="https://www.truegether.com/eBayAlternative.html" rel="noopener noreferrer"&gt;eBay alternative&lt;/a&gt; built around a zero-fee model for sellers. This post's about the Listing Details page: the single page every buyer lands on before deciding whether to buy something on TrueGether, and the one we were most nervous about touching.&lt;/p&gt;

&lt;p&gt;We rebuilt it in React, Vite, TypeScript, and Tailwind. The stack decision wasn't the hard part. What actually took the time was everything the old page was quietly doing that nobody had written down anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the old page was actually carrying&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pattern is probably familiar to anyone who's rebuilt a page that's been patched for years: small fixes accumulate into undocumented behavior. Small fixes can accumulate into undocumented behavior: a spacing rule that only matters for one product category, or logic that was added for an old edge case and never removed because nobody was sure what would break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Vite mattered more than we expected&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The concrete effect was on how willing people were to iterate during review. A slow feedback loop doesn't just cost time, it changes behavior — people stop trying small improvements when every attempt costs thirty seconds of waiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript found problems that weren't type problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was the real surprise. Adding types to a page handling prices, variants, inventory, and seller data didn't just catch mismatches — it forced us to actually agree on what shape that data was supposed to be. The important part wasn't any single mismatch. It was that the type system forced us to make assumptions explicit instead of letting them remain implicit. The type system didn't cause those problems. It just made them impossible to keep ignoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Tailwind migration cost was about unlearning, not learning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The class-name syntax isn't the hard part of adopting Tailwind. The hard part is years of CSS instinct — reaching for a new stylesheet rule instead of composing utility classes — not disappearing just because the tooling changed. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we'd do differently&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The risk in a rewrite like this was never really the new code — it was the undocumented behavior sitting in the old code that nobody had mapped out. That's the main lesson we're carrying into the next piece of TrueGether's infrastructure we touch: map it before you migrate it, not during.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Lessons From Building a Zero-Fee eBay Alternative</title>
      <dc:creator>Saloni Jain</dc:creator>
      <pubDate>Fri, 25 Sep 2026 15:02:21 +0000</pubDate>
      <link>https://dev.to/saloni_jain_811f1b2d26c8d/lessons-from-building-a-zero-fee-ebay-alternative-4403</link>
      <guid>https://dev.to/saloni_jain_811f1b2d26c8d/lessons-from-building-a-zero-fee-ebay-alternative-4403</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7rfwes6jao53stj71e0p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7rfwes6jao53stj71e0p.png" alt="Isometric dark-themed illustration of marketplace backend architecture — a search interface, laptop and phone, server stack, and database connected by glowing teal lines, with product category icons for a shirt, sneakers, a bag, and headphones" width="" height=""&gt;&lt;/a&gt;&lt;br&gt;
I work on &lt;a href="https://www.truegether.com/" rel="noopener noreferrer"&gt;TrueGether&lt;/a&gt;, a marketplace that's been running since 2012 where you can sell an item with no commission taken out — no listing fee either, unlike most of the platforms sellers are used to. I didn't build the business model, but I've spent a lot of time on the engineering side of what that pricing model actually forces you to get right, and I wanted to write down what that's taught me, mistakes included.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Zero fee" is a product constraint, not just a pricing page&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first thing that becomes obvious once "no commission" is the pitch: you can't paper over friction with money. A marketplace that charges 10-13% per sale can afford a clunky onboarding flow, because the revenue is already flowing. We don't have that cushion, so every point of friction in listing, searching, and checkout has to earn its keep on its own. That reframes a lot of otherwise "nice to have" engineering work as actually load-bearing for the business.&lt;/p&gt;

&lt;p&gt;The clearest example: a huge number of sellers who'd consider a second marketplace already have a working catalog somewhere else — usually eBay or Shopify. Asking them to manually re-list everything is the single biggest thing standing between "interested" and "actually selling." So instead of just building a listing form and hoping, we built import tooling that pulls an existing eBay or Shopify catalog in directly. It's not glamorous work, but it probably did more for adoption than any feature we shipped that quarter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search gets weird when your catalog is everything&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TrueGether spans a genuinely wide catalog — electronics, jewelry, collectibles, home goods, sporting goods, and a couple dozen categories beyond that. That's great for being a real "one place to list" marketplace, but it's rough on search relevance. A query like "case" means something completely different in Electronics (phone case) than in Musical Instruments (guitar case) than in Collectibles (display case). Generic relevance tuning that works for a single-category shop falls apart fast at that breadth.&lt;/p&gt;

&lt;p&gt;We run Solr for search, and most of the actual engineering effort there isn't indexing — it's building enough category-aware signal into ranking that a broad, ambiguous query still surfaces the right cluster of results instead of a blend of unrelated ones. It's the kind of problem that looks small until you're staring at a search log full of near-misses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching a marketplace is not caching a blog&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Varnish sits in front of a lot of our read traffic, and for a mostly-static site that's a solved problem. A marketplace is not mostly static — prices change, items sell out, sellers edit listings — so overly aggressive caching means showing a buyer something that's no longer true, and overly cautious caching means your origin gets hammered on every category page. We ended up spending more time on invalidation strategy (what has to bust the cache immediately vs. what can lag by a few minutes) than on the caching layer itself, which was the opposite of what I expected going in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rewrite less than you think you need to&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some of the frontend is still JSP. Some of it — the listing details page, most recently — has been rebuilt in React, TypeScript, and Tailwind. I used to be a "just rewrite it properly" person, and working on a live marketplace cured me of that. You can't take checkout offline for a quarter to do a clean rewrite, so the actual skill is figuring out which single page or flow to modernize next, shipping it behind the old one, and letting the two coexist without users noticing the seam. It's slower and less satisfying than a fresh start, but it's the only version of "modernize the stack" that doesn't put revenue at risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd tell someone building a marketplace from scratch&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;None of this is exotic technology — Solr, HBase, Varnish, Docker, a slow React migration. The lessons weren't really about picking the right tools, they were about which problems actually matter when your pricing model removes the usual safety net. Building a real &lt;a href="https://www.truegether.com/eBayAlternative.html" rel="noopener noreferrer"&gt;eBay alternative&lt;/a&gt; — not just a cheaper clone with the same fee structure — turned out to be less about the pricing page and more about all of the above. If you're not charging fees, your infrastructure has to work harder to justify why anyone stays, because you can't buy their patience with a good deal — the deal is the default state.&lt;/p&gt;

&lt;p&gt;If you want to see where all of this actually landed, the same import-and-list flow I described above kicks in the moment you become an online seller there — it's running in production, not just on a roadmap slide.&lt;/p&gt;

&lt;p&gt;Happy to go deeper on any of this in the comments — search relevance and cache invalidation are the two I'd genuinely enjoy nerding out about further.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>showdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>Why We Built One-Click Catalog Import for Sellers Switching Marketplaces</title>
      <dc:creator>Saloni Jain</dc:creator>
      <pubDate>Tue, 18 Aug 2026 11:54:32 +0000</pubDate>
      <link>https://dev.to/saloni_jain_811f1b2d26c8d/why-we-built-one-click-catalog-import-for-sellers-switching-marketplaces-4m66</link>
      <guid>https://dev.to/saloni_jain_811f1b2d26c8d/why-we-built-one-click-catalog-import-for-sellers-switching-marketplaces-4m66</guid>
      <description>&lt;p&gt;Every marketplace pitch to sellers eventually runs into the same wall: "okay, but I have 400 listings on eBay already."&lt;/p&gt;

&lt;p&gt;That single sentence kills more platform switches than pricing, trust, or features combined. Asking someone to manually recreate hundreds of titles, photos, prices, and descriptions isn't a small ask — it's a weekend of unpaid work before they've sold a single item. So when we were building TrueGether, a marketplace positioned as a practical &lt;a href="https://www.truegether.com/eBayAlternative.html" rel="noopener noreferrer"&gt;eBay alternative&lt;/a&gt; for people who want to start selling online, new or used, without giving up a cut of every sale, we knew the "empty store" problem would matter more than almost anything else in the product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The real friction isn't fees, it's the migration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We'd assumed price would be the deciding factor for sellers. It wasn't. In early conversations, the objection that came up again and again was some version of: "I believe you're cheaper, but I'm not starting from zero." Sellers who'd spent years building out a catalog on eBay or a Shopify store weren't going to abandon that catalog just to test a new platform, no matter how good the pitch to sell online without fees sounded on paper.&lt;/p&gt;

&lt;p&gt;That reframed the problem. It wasn't "how do we convince sellers to switch," it was "how do we make switching cost nothing."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we built: listings that carry over intact&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We ended up building an import path that pulls existing listings in directly from eBay and Shopify — titles, prices, photos, quantities, and descriptions come across instead of needing to be rebuilt by hand. The seller reviews and confirms rather than starting from a blank form.&lt;/p&gt;

&lt;p&gt;The unglamorous part of this work was category mapping. eBay and Shopify each have their own sprawling, inconsistent taxonomy, and none of it lines up cleanly with a simpler category structure. A listing sitting three levels deep in eBay's &lt;a href="https://www.truegether.com/category/antiques" rel="noopener noreferrer"&gt;antiques&lt;/a&gt; tree doesn't have an obvious 1:1 home — we had to decide where the mapping should be strict versus where it made more sense to fall back to a broader parent category and let the seller fine-tune from there.&lt;/p&gt;

&lt;p&gt;Photos were the other place small decisions mattered. Keeping image order and primary-photo selection intact sounds trivial until you're moving hundreds of listings at once and one broken mapping means every photo in a shop is off by one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reputation had to move too, not just inventory&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Listings were only half the migration problem. A seller with 1,200 reviews and a 4.9 rating on eBay isn't just losing a catalog by switching platforms — they're losing years of earned trust and starting over as an anonymous new account. For anyone who's spent years building a track record, that's a bigger deterrent than rebuilding listings ever was.&lt;/p&gt;

&lt;p&gt;So the import path also carries over the seller's existing ratings and reviews — not just an aggregate score, but the actual reviews customers left. A buyer looking at a "new" seller on TrueGether can still see the reputation that seller earned somewhere else, instead of everyone starting from a blank slate regardless of track record.&lt;/p&gt;

&lt;p&gt;This ended up being the harder design problem of the two. Listings are static data — copy the fields, map the categories, done. Reputation is trust, and trust only means something if it's clearly attributed and verifiably tied to the account that earned it. Getting that balance right mattered more than getting the import mechanically working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this mattered more than a fee comparison&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;None of this shows up in a "compare our fees to eBay's fees" pitch, but it's the actual product of taking migration seriously — not just cheaper, but nothing lost in the switch, neither catalog nor reputation. A cheaper platform that still costs someone a weekend of manual work and a blank reputation isn't actually cheaper — it just moves the cost from money to time and trust.&lt;/p&gt;

&lt;p&gt;If you're building anything that asks users to migrate from an incumbent platform, the lesson generalized further than we expected: the technical lift of a clean, trust-preserving import isn't a nice-to-have feature, it's often the actual decision that determines whether anyone tries you at all.&lt;/p&gt;

&lt;p&gt;If you're weighing TrueGether as your next storefront or just exploring it as a side hustle selling platform, you can see how listings and reviews come across on the &lt;a href="https://www.truegether.com/sell.html" rel="noopener noreferrer"&gt;seller signup page&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;FAQ&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. What exactly gets imported from an eBay or Shopify store?&lt;/strong&gt;&lt;br&gt;
Titles, prices, photos, quantities, and descriptions carry over, along with category mapping. The seller reviews everything before it goes live rather than it publishing automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Do reviews transfer too, or just listings?&lt;/strong&gt;&lt;br&gt;
Both. Along with the catalog, existing ratings and reviews carry over — not just an aggregate score, but the actual reviews customers left — so a seller's track record isn't reset by switching platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Which platforms can sellers import from right now?&lt;/strong&gt;&lt;br&gt;
eBay and Shopify are supported today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Is there a cost to use the import tool?&lt;/strong&gt;&lt;br&gt;
No — it's part of the same zero-fee model as the rest of the platform, so there's no separate charge to bring a catalog or reputation over.&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>startup</category>
      <category>buildinpublic</category>
      <category>marketplace</category>
    </item>
    <item>
      <title>Designing a Category Taxonomy That Doesn't Collapse Under Its Own Weight</title>
      <dc:creator>Saloni Jain</dc:creator>
      <pubDate>Mon, 27 Jul 2026 13:07:21 +0000</pubDate>
      <link>https://dev.to/saloni_jain_811f1b2d26c8d/designing-a-category-taxonomy-that-doesnt-collapse-under-its-own-weight-5gaj</link>
      <guid>https://dev.to/saloni_jain_811f1b2d26c8d/designing-a-category-taxonomy-that-doesnt-collapse-under-its-own-weight-5gaj</guid>
      <description>&lt;p&gt;Every e-commerce or content platform eventually hits the same wall: the category tree that made sense with 200 items turns into an unmanageable mess at 200,000. A few of the actual design problems, and what tends to work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flat categories don't scale, but neither does infinite nesting.&lt;/strong&gt; A single flat list of 'Electronics, &lt;a href="https://www.truegether.com/category/clothing-accessories" rel="noopener noreferrer"&gt;Clothing&lt;/a&gt;, Home Goods' works fine at small scale and becomes useless once you have thousands of items per category — nobody can browse 40,000 'Electronics' listings. But going the other direction and nesting 6 levels deep (Electronics &amp;gt; Audio &amp;gt; Headphones &amp;gt; Wireless &amp;gt; Over-Ear &amp;gt; Noise Cancelling) creates a different problem: sellers guess wrong about where their item belongs, and half your inventory ends up misfiled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Items rarely fit exactly one category.&lt;/strong&gt; A wireless speaker with Bluetooth and a built-in flashlight is legitimately both 'Audio' and 'Outdoor Gear.' Most systems force a single primary category and treat everything else as a tag, but that means your category-page filtering logic and your tag-based search logic need to actually agree with each other, which is harder than it sounds once both systems evolve independently — general marketplaces spanning categories from electronics to home goods run into this constantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Category taxonomy is a data migration problem disguised as a UX problem.&lt;/strong&gt; The moment you need to split 'Home &amp;amp; Garden' into 'Home Decor' and 'Garden &amp;amp; Outdoor' because one category got too large, every existing listing, every saved search, every analytics dashboard built around the old taxonomy breaks or silently miscounts. Versioning your taxonomy (so you can query 'what did this look like as of date X') ends up mattering a lot more than it seems like it should at design time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attribute-based filtering is often a better mental model than category depth.&lt;/strong&gt; Instead of drilling deeper into subcategories, letting users filter by attributes (size, condition, brand, color) within a shallower category tree tends to actually match how people search, and it scales better as inventory grows, since you're adding filterable fields instead of restructuring the whole tree.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnc65e4i0lm6fdgik3d2t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnc65e4i0lm6fdgik3d2t.png" alt=" " width="799" height="355"&gt;&lt;/a&gt;&lt;br&gt;
This is the kind of problem that looks trivial in a database schema diagram and turns into a genuine architecture decision once real inventory volume hits it — I've seen this come up in general marketplaces (TrueGether, an &lt;a href="https://www.truegether.com/eBayAlternative.html" rel="noopener noreferrer"&gt;alternative to eBay&lt;/a&gt;, is one example of a platform spanning electronics-to-home-goods categories that has to solve this) and in any CMS with more than a couple hundred content types.&lt;/p&gt;

&lt;p&gt;Curious how others have handled the single-vs-multi-category problem specifically — forcing one primary category plus tags, or building genuine multi-category membership from the start?&lt;/p&gt;

</description>
      <category>systemdesign</category>
      <category>database</category>
      <category>ecommerce</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
