We implement a system for aggregating products from multiple suppliers: a single product card with the best offer selection. No duplicates, with up-to-date prices and stock. Imagine: you have 10 suppliers, each with their own catalog. Products overlap by 70%, but SKUs differ. Prices change daily. The customer sees one card, and behind it — a choice of 5 offers with different prices and delivery times. We'll tell you how to build this without duplication and errors. We rely on 10 years of experience in integrating e-commerce solutions. We guarantee stable operation with 100,000+ products. Average savings after implementation — from 200,000 RUB per year.
The Difference Between Import and Aggregation
Import is saving data 'as is'. Aggregation is building a storefront over raw data from multiple suppliers. With import, you risk getting duplicates and outdated prices. Aggregation gives a single card with automatic selection of the best offer.
| Criterion | Import | Aggregation |
|---|---|---|
| Data | As is from each supplier | Single normalized card |
| Price | Multiple records per product | One card with choice |
| Update | Overwrite/add | Automatic recalculation of best offer |
How to Identify Identical Products from Different Suppliers?
Identification is the key task of aggregation. We use several levels of matching: exact match by article (supplier_sku), matching by barcode, and, if necessary, fuzzy name comparison using Levenshtein distance. In the master card (products), the master_sku is stored, to which all offers (product_offers) from different suppliers are linked. Matching accuracy reaches 99% when articles are available.
How to Set Up Aggregation? Step-by-Step Plan
- Analyze supplier data — collect formats, identify overlaps and unique attributes.
- Design schema — create tables 'products' and 'product_offers', set up indexes.
- Implement matching — write matching algorithms with fuzzy search support.
- Develop BestOfferResolver — configure scoring considering price, stock, and delivery time.
- Integrate storefront — prepare API with aggregated fields.
Data Schema for Aggregation
-- Master card (aggregated)
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
master_sku VARCHAR(255) UNIQUE NOT NULL,
name TEXT NOT NULL, -- from the "main" supplier
description TEXT,
attributes JSONB DEFAULT '{}',
category_id INT REFERENCES categories(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Supplier offers linked to master card
CREATE TABLE product_offers (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(id),
supplier_id INT NOT NULL REFERENCES suppliers(id),
supplier_sku VARCHAR(255) NOT NULL,
price NUMERIC(12,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
lead_time_days SMALLINT, -- delivery time in days
is_primary BOOLEAN DEFAULT FALSE, -- content source for the card
last_synced_at TIMESTAMP,
UNIQUE(supplier_id, supplier_sku)
);
-- Indexes for fast best offer search
CREATE INDEX idx_offers_product_price ON product_offers(product_id, price)
WHERE stock > 0;
How to Select the Best Offer on the Storefront?
The best offer is determined by configurable rules. The typical option is the minimal price among suppliers with stock. We also use scoring with weighted coefficients (price, stock, delivery time), which allows more precise consideration of business priorities. Our scoring method is 2 times more accurate than simple minimum price selection.
class BestOfferResolver
{
public function resolve(int $productId): ?ProductOffer
{
return ProductOffer::where('product_id', $productId)
->where('stock', '>', 0)
->orderByRaw('
price * (1 + COALESCE(
(SELECT markup FROM suppliers WHERE id = supplier_id), 0
) / 100)
')
->orderBy('lead_time_days')
->first();
}
}
Aggregated Storefront in API
The API response for a product card should include aggregated data: minimum and maximum price, total stock, as well as a list of offers for the buyer to choose from.
class ProductResource extends JsonResource
{
public function toArray($request): array
{
$bestOffer = $this->bestOffer;
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'attributes' => $this->attributes,
// Aggregated prices
'price' => $bestOffer?->price,
'price_min' => $this->offers->where('stock', '>', 0)->min('price'),
'price_max' => $this->offers->where('stock', '>', 0)->max('price'),
'in_stock' => $this->offers->where('stock', '>', 0)->count() > 0,
'total_stock' => $this->offers->sum('stock'),
// List of offers (if the store shows them explicitly)
'offers' => OfferResource::collection(
$this->offers->where('stock', '>', 0)->sortBy('price')
),
];
}
}
Updating Aggregation When Offers Change
Aggregated values must be updated on every change of a supplier offer. We use an Observer that invalidates cache and recalculates denormalized fields.
class ProductOfferObserver
{
public function saved(ProductOffer $offer): void
{
// Recalculate aggregates in cache
Cache::forget("product.{$offer->product_id}.best_offer");
Cache::forget("product.{$offer->product_id}.price_range");
// Update denormalized fields in products
$this->recalculate($offer->product_id);
}
private function recalculate(int $productId): void
{
$agg = ProductOffer::where('product_id', $productId)
->where('stock', '>', 0)
->selectRaw('MIN(price) as min_price, MAX(price) as max_price, SUM(stock) as total_stock')
->first();
Product::where('id', $productId)->update([
'price_min' => $agg->min_price,
'price_max' => $agg->max_price,
'total_stock' => $agg->total_stock,
'updated_at' => now(),
]);
}
}
Displaying Multiple Offers on the Product Card
If the business logic allows the buyer to choose a supplier (like on Yandex.Market), we use a React component for the list of offers.
// React component for the list of offers
const OfferList: React.FC<{ offers: Offer[] }> = ({ offers }) => {
const sorted = [...offers].sort((a, b) => a.price - b.price);
return (
<div className="space-y-2">
{sorted.map(offer => (
<div key={offer.id} className="flex items-center justify-between border rounded p-3">
<div>
<span className="font-semibold">{formatPrice(offer.price)}</span>
<span className="text-sm text-gray-500 ml-2">
{offer.supplier.name}
</span>
</div>
<div className="text-sm text-gray-500">
{offer.stock > 0
? `in stock ${offer.stock} pcs.`
: 'out of stock'}
{offer.lead_time_days && ` · delivery ${offer.lead_time_days} days`}
</div>
<button
onClick={() => addToCart(offer)}
disabled={offer.stock === 0}
className="btn-primary"
>
Buy
</button>
</div>
))}
</div>
);
};
Typical Mistakes in Aggregation
- Lack of normalization — storing raw supplier data without a master card leads to duplicates and confusion.
- Ignoring weights — selecting only the minimum price without considering stock and delivery time reduces conversion.
- Synchronization without Observer — manually updating aggregates leads to stale data.
Comparison of Approaches: Manual vs Automated Aggregation
| Parameter | Manual Aggregation | Automated Aggregation (our solution) |
|---|---|---|
| Update time | hours/days | real time or delayed (15 min) |
| Matching accuracy | 80-90% | 99% when articles are present |
| Maintenance cost | High (human resources) | Low (server resources) |
Economic Efficiency
Automated aggregation reduces catalog maintenance costs by up to 40%. Typical savings for a catalog of 50,000 products is from 200,000 RUB per year. When scaling to 200,000 products, savings exceed 500,000 RUB.
What's Included in the Work
We take on turnkey implementation: from architecture design to deployment. Within the project you get:
- normalized database schema (master cards + offers);
- API resources for the storefront with caching;
- frontend components for supplier selection;
- Elasticsearch indexing setup (if needed);
- maintenance documentation and team training.
Implementation Timeline
Approximate timelines for stages:
- Schema + merging logic + BestOfferResolver: 2 days
- Observer + aggregate denormalization: 1 day
- API resource with offers + frontend component: 1–2 days
- Elasticsearch integration: +2 days
- Weight coefficient configuration via admin panel: +1 day
Basic aggregation without search: 4–5 working days. Contact us for a consultation — we'll prepare architecture and accurate timelines for your project. Request an estimate to find out how aggregation can reduce catalog maintenance costs. Check your catalog: if you spend more than 10 hours a week on manual price updates, our solution pays for itself in 2-3 months. Get an engineer consultation — we'll calculate savings for your project.







