Build1 publisherNot yet confirmed elsewhere3 min readPublished
Two marketplaces, four broken methods: the shared Marketplace interface is the wrong abstraction
Ozon and Wildberries disagree about identifiers, batch semantics and how failure is signalled. In one Go codebase, the only shared machinery on show is the transport.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction
What happened
- The task is framed as a textbook polymorphism example: several platforms, the same actions, list the cards, push the stock, push the price, pull the orders. The author states that at the end one thing turned out to be genuinely shared, and not the one he would have bet on.
- The proposed Go interface is: type Marketplace interface { ListOffers() ([]Offer, error); SetStock(offerID string, n int64) error; SetPrice(offerID string, price int64) error; Orders(since time.Time) ([]Order, error) }.
- The team wrote Ozon, then Wildberries, and that interface fell apart on every one of its four methods.
- Ozon and Wildberries are described as the two largest marketplaces in Russia and the CIS.
- On Ozon a product is an offer_id, the seller's own article number uploaded to the cabinet; both stock and price are set by it. The link table is one row: CREATE TABLE ozon_links (product_id INTEGER PRIMARY KEY, offer_id TEXT NOT NULL, ...).
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A team building a self-hosted seller store in Go published its integration notes for Ozon and Wildberries, and the finding is negative: the obvious four-method `Marketplace` interface, `ListOffers` / `SetStock` / `SetPrice` / `Orders`, fell apart on every one of the four [2] [3]. That is worth reading if you have ever sketched that interface because "the actions are the same across platforms" [1], because the breakage is in the identifiers and the response shapes, not in the verbs.
Start with keys. On Ozon a product is an `offer_id`, your own article number uploaded to the cabinet, and both stock and price are set by it, so the link table is one row with one text column [4]. On Wildberries a card is an `nmID`, a card has sizes, and a size has barcodes: stock is set per barcode, price per card [5]. Both identifiers have to be stored, with a unique index on barcode and a non-unique one on `nmID`, because several rows sharing an `nmID` are just the sizes of one card, while two products sharing a barcode would push two different levels into the same slot on every pass [6] [7]. One key on one platform, two on the other [21]: `SetStock(offerID string, n int64)` has nowhere to put the second, and which key you pass depends on the operation [8].
Then the return values. According to the write-up, Ozon answers a stock push with a per-item result list carrying `OfferID`, `Updated` and errors, and the team treats an item the platform said nothing about as not pushed, on the reasoning that remembering a level the platform never received is worse than sending it twice [9] [10]. Wildberries answers `PUT /api/v3/stocks/{warehouseId}` with 204 and an empty body, crediting the entire batch at once, and names offenders only in a 409, only the ones that failed [11]. So one client returns `[]ItemResult` and the other `map[string]string`; force them into a common type and half its fields are always empty [12] [13]. The client also refuses to credit a batch when the error is not a 409 API error, on the grounds that the transport died [14].
Failure detection is worse than the shape mismatch. Some Wildberries methods return 200 with `{"error": true, "errorText": "..."}` in the body, which Ozon does not do; miss the check and the push is recorded as successful and never retried [15]. That check therefore sits in the transport rather than in each method, and the same choke point strips the token out of platform text before it reaches a log or the database, on the theory that one line is cheaper than establishing whether the API echoes requests back [16] [17].
Price is where the abstraction stops being salvageable. Ozon gives a verdict per item immediately, so a row's state is one number, the last value pushed, guarded by SQL that rounds up to whole roubles inside the comparison [18] [19]. The platform accepts whole roubles, so `price_pushed` holds the rounded value, and comparing raw kopecks against it marks every row as changed on every pass [19]; in integer arithmetic `(price + 99) / 100 * 100` turns 12301 kopecks into 12400 [22]. That rounding rule is platform policy leaking into your database schema, which no interface method signature can absorb.
Two things to watch. The post says one thing did turn out to be genuinely shared, and the excerpt available breaks off before naming it; the only common machinery visible in the code shown is the transport layer, doing envelope checks, error classification and token scrubbing [1] [16] [17]. And the unique barcode index [6] is a bet on seller discipline: it will be the first thing to fail in a catalogue where the same barcode was pasted onto two products.