Каталог, корзина и оформление заказа
Основа любого интернет-магазина: товары, цены, корзины и заказы, которые никогда не врут.
За 30 секунд
Интернет-магазин — это три системы, склеенные вместе: каталог, который описывает, что вы продаёте, корзина, которая помнит, что хочет покупатель, и оформление заказа (checkout), которое превращает это желание в оплаченный и отслеживаемый заказ. Если с самого начала правильно спроектировать модель данных — варианты товаров, деньги в целых числах, явную машину состояний заказа, — это сэкономит месяцы болезненных исправлений в будущем.
Почему я это использую
- Я использую эту структуру, потому что строил её снова и снова: заказ еды в ресторанах, товары для животных, косметика, вино, мерч, продукты и туры — всё сводится к одному и тому же конвейеру каталог → корзина → заказ.
- Большинство продакшен-багов в магазинах вовсе не экзотические: ошибка округления в цене, корзина, которая теряет товары после входа в аккаунт, заказ, оплаченный дважды, проданный товар, которого уже нет на складе. Чистая модель убирает целые классы таких ошибок.
- Оформление заказа — это место, где бизнес зарабатывает деньги. Каждый лишний шаг или непонятная ошибка стоит реальных заказов, поэтому этот участок заслуживает внимания уровня senior.
- Когда ядро надёжно, поверх него можно добавлять акции, программу лояльности, синхронизацию с ERP и мобильные приложения, не переписывая его.
Как это работает
01Каталог: товары, варианты и атрибуты
Представьте товар как ценник на полке («Лак для ногтей»), а вариант — как конкретную вещь, которую кладут в коробку («Лак для ногтей, красный, 15 ml»). Покупатели просматривают товары, но продаёте, храните на складе и оцениваете вы именно варианты — у каждого варианта свой SKU (stock-keeping unit, уникальный артикул), цена и остаток. Атрибуты описывают варианты: цвет, размер, вес, вкус. Если сгруппировать атрибуты в семейства («одежда» — размер и цвет, «корм для животных» — вес и вкус), админка останется вменяемой. Категории образуют дерево (Собаки → Корм → Сухой корм); храните его как nested set или как parent_id плюс закэшированный путь, чтобы «все товары в разделе Собаки» получались одним запросом. Бренды, теги и подборки — это отдельные связи many-to-many, а не дополнительные категории.
- Товар: название, описание, изображения, SEO-поля, связи с категориями.
- Вариант: SKU, значения атрибутов, цена, вес, штрихкод, связь с остатками.
- Некоторые товары продаются упаковками или на вес — моделируйте единицу продажи явно, а не через хаки с количеством.
02Деньги: целые числа, валюты и налоги
Никогда не храните деньги во float. В двоичной арифметике с плавающей точкой 0.1 + 0.2 не равно 0.3, и эти крошечные ошибки складываются в реальные расхождения в бухгалтерии. Храните суммы как целые числа в минимальных единицах (центы или целые драмы для валют без дробных единиц) вместе с кодом валюты по ISO 4217, а вычисления делайте через библиотеку для денег или value object. Один раз решите, включают ли цены в каталоге налог (обычно так в потребительских магазинах Европы, Армении и Австралии) или нет (обычно так в США), и считайте налог по каждой строке с понятным правилом округления. Сохраняйте снимок каждой цены, скидки и налога в строке заказа: каталог завтра изменится, а заказ всё равно должен показывать то, что заплатил покупатель.
// Money as a small value object: integer minor units + currency
type Money = { amount: number; currency: "USD" | "EUR" | "AMD" };
const add = (a: Money, b: Money): Money => {
if (a.currency !== b.currency) throw new Error("Currency mismatch");
return { amount: a.amount + b.amount, currency: a.currency };
};
// Tax included in the price: extract it with one explicit rounding rule
const taxIncluded = (gross: Money, ratePercent: number): Money => ({
amount: Math.round((gross.amount * ratePercent) / (100 + ratePercent)),
currency: gross.currency,
});
const line = { amount: 1999, currency: "USD" } as const; // $19.99
console.log(taxIncluded(line, 20)); // { amount: 333, currency: "USD" }
03Корзина: гости, слияние и валидация
Позвольте людям делать покупки без аккаунта. Гостевая корзина живёт на сервере и идентифицируется случайным токеном, который хранится в cookie (веб) или в защищённом хранилище (приложение). Если корзина на сервере, цены и правила считаются в одном месте, а после входа корзина следует за пользователем на всех устройствах. Когда гость входит в аккаунт, выполните слияние: добавьте гостевые строки в корзину аккаунта, объедините количества для одного и того же варианта, затем удалите гостевую корзину. Относитесь к корзине как к пожеланию, а не к обещанию. Каждый раз при чтении — и обязательно при оформлении заказа — перепроверяйте её: активен ли ещё вариант, хватает ли остатка, не изменилась ли цена, открыт ли филиал, набрана ли минимальная сумма заказа? Возвращайте понятные сообщения («Цена изменилась с 12.00 на 13.50»), а не исправляйте всё молча.
- Храните в корзине ID вариантов и количества, а не цены, присланные клиентом.
- Пересчитывайте итоги на сервере в каждом ответе.
- Предложите повтор заказа: скопируйте строки из прошлого заказа в корзину, а затем проверьте их как обычно.
04Оформление заказа и машина состояний
Checkout собирает всё, что нужно заказу, — контакты, адрес или пункт самовывоза, время доставки, способ оплаты — и затем создаёт заказ в одной транзакции базы данных: копирует строки корзины со снимками цен, резервирует остатки, сохраняет итоги и ставит статус «ожидает оплаты». С этого момента заказ движется по явной машине состояний. Машина состояний — это просто список допустимых статусов и допустимых переходов между ними. Если записать её явно, не случится абсурда вроде отменённого заказа, который вдруг стал доставленным, а каждый переход логируется в таблицу истории статусов с указанием, кто и когда его сделал. Побочные эффекты — письма, push-уведомления, экраны на кухне, экспорт в ERP — вешаются на переходы, в идеале через задачи в очереди.
const transitions = {
pending_payment: ["paid", "cancelled"],
paid: ["preparing", "refunded"],
preparing: ["ready", "cancelled"],
ready: ["out_for_delivery", "picked_up"],
out_for_delivery: ["delivered", "failed_delivery"],
delivered: ["refunded"],
} as const;
type Status = keyof typeof transitions | "cancelled" | "refunded" | "picked_up" | "failed_delivery";
export function canMove(from: Status, to: Status): boolean {
const allowed = (transitions as Record<string, readonly string[]>)[from] ?? [];
return allowed.includes(to);
}
05Доставка, самовывоз и временные слоты
Исполнение заказа — это то, чем магазины отличаются сильнее всего. Для доставки нужны адрес, зона и стоимость. Зоны обычно задаются полигонами на карте: геокодируйте адрес в координаты, затем проверьте, какой полигон содержит эту точку, — так вы получите стоимость доставки, минимальную сумму заказа и обслуживающий филиал. Для самовывоза (takeaway) нужен филиал и, возможно, список товаров, который отличается от филиала к филиалу. Временные слоты превращают часы работы в окна, которые можно выбрать: «как можно скорее» или на определённое время с шагом 15 или 30 минут, с учётом времени приготовления, времени закрытия и «рабочего дня», который может заканчиваться после полуночи. Генерируйте слоты на сервере, в часовом поясе магазина, и перепроверяйте выбранный слот при оформлении — слот, свободный при загрузке страницы, через минуту может оказаться занят.
06Защита от перепродажи и правильные письма
Перепродажа (overselling) случается, когда два покупателя одновременно покупают последний товар. Решение — сделать изменение остатка атомарным. Либо заблокируйте строку (SELECT … FOR UPDATE внутри транзакции), либо используйте условное обновление, которое срабатывает только при достаточном остатке, и проверяйте количество затронутых строк. Резервируйте остаток при создании заказа и освобождайте его, если оплата не прошла или истекло время; задача по расписанию отменяет брошенные заказы в статусе ожидания. Транзакционные письма — заказ получен, оплачен, отправлен, возвращён — нужно отправлять из очереди после коммита транзакции, чтобы медленный почтовый сервер никогда не блокировал checkout и чтобы не ушло письмо о заказе, транзакция которого откатилась.
-- Atomic decrement: succeeds only when enough stock is left
UPDATE stock_levels
SET quantity = quantity - 2
WHERE variant_id = 42
AND warehouse_id = 1
AND quantity >= 2;
-- 1 row affected → reserved; 0 rows → out of stock, tell the customer
Когда применять
- Вы строите магазин с реальными бизнес-правилами (варианты, зоны доставки, акции, ERP), с которыми готовые платформы борются.
- Один и тот же каталог должен обслуживать сайт, мобильное приложение и админку через один API.
- Заказы должны интегрироваться с бухгалтерией, фискальными чеками, кухнями или складами.
- Вам нужен полный контроль над UX оформления заказа и владение данными.
Когда не стоит
- Небольшой магазин со стандартными товарами и без интеграций — хостинговая платформа вроде Shopify быстрее и дешевле.
- У вас нет команды, которая будет поддерживать платежи, обновления безопасности и интеграции после запуска.
- Бизнес-модель ещё не проверена; сначала проверьте её на хостинговом магазине.
Альтернативы
- Shopify
Хостинговая платформа, быстрый запуск, сильная экосистема; кастомная логика живёт в приложениях и расширениях checkout, у которых есть ограничения.
- WooCommerce
Плагин для WordPress, дешёвый и гибкий, но производительность и качество кода сильно зависят от плагинов.
- Medusa / Saleor
Open-source headless-движки для e-commerce — хорошая золотая середина, когда нужен контроль над кодом, но не хочется начинать с нуля.
- commercetools
Enterprise headless commerce API; мощно, но по цене и требованиям к команде рассчитано на крупные компании.
Лучшие практики
- Храните деньги как целые числа плюс код валюты; никогда не во float.
- Сохраняйте снимки цен, скидок, налогов и названий товаров в строках заказа.
- Пересчитывайте все итоги на сервере; клиент присылает только ID и количества.
- Моделируйте статусы заказа как явную машину состояний с таблицей истории.
- Делайте изменения остатков атомарными и освобождайте резервы при неудачной или просроченной оплате.
- Отправляйте письма и интеграции из задач в очереди после коммита транзакции.
- Храните гостевую корзину на сервере и сливайте её при входе в аккаунт.
Частые ошибки
- Использовать float-колонки для цен и потом находить расхождения в центах в отчётах.
- Читать цены из текущего товара вместо снимка в заказе, из-за чего старые счета меняются.
- Доверять итоговой сумме, присланной браузером или приложением.
- Менять статус заказа произвольными обновлениями, что приводит к невозможным состояниям.
- Проверять остаток только при добавлении в корзину и не проверять повторно при оформлении.
Глубже
07Идемпотентное создание заказа: как пережить двойные клики и повторы
Покупатель дважды нажимает «Оформить заказ», мобильное приложение повторяет запрос после таймаута, балансировщик нагрузки переотправляет запрос. Без защиты каждый такой случай может создать второй заказ и дважды зарезервировать остаток. Решение — checkout key: клиент генерирует случайный UUID при открытии экрана оформления и отправляет его вместе с запросом. Сервер сохраняет его в заказе под уникальным ограничением. Первый запрос создаёт заказ; любой повтор с тем же ключом возвращает уже существующий заказ вместо создания нового. Храните ключ на протяжении всей попытки оформления и генерируйте новый только при изменении корзины. Дополните это короткой блокировкой на корзину (Redis lock или advisory lock), чтобы два параллельных запроса не прошли валидацию раньше, чем один из них сделает вставку.
-- One order per checkout attempt, enforced by the database
ALTER TABLE orders ADD COLUMN checkout_key UUID;
CREATE UNIQUE INDEX orders_checkout_key_uq ON orders (checkout_key);
-- Inside the checkout transaction
INSERT INTO orders (checkout_key, customer_id, status, total_amount, currency)
VALUES ($1, $2, 'pending_payment', $3, $4)
ON CONFLICT (checkout_key) DO NOTHING
RETURNING id;
-- No row returned → a previous request already created it:
-- SELECT id FROM orders WHERE checkout_key = $1 and return that order.
08Распределение скидок и округление без потери копейки
Скидку на всю корзину («5.00 с заказа») нужно разложить по строкам, потому что налоги, возвраты и выгрузки в ERP работают построчно. Делите её пропорционально стоимости каждой строки, округляйте каждую долю вниз, а оставшиеся центы отдайте строкам с наибольшими остатками. Доли всегда в сумме дают точную скидку, а последующий частичный возврат одной строки вернёт ровно то, что за неё списали. Также учитывайте минимальную единицу валюты из ISO 4217: у большинства валют 2 знака после запятой, у японской иены 0, а у кувейтского динара 3. Никогда не хардкодьте «делим на 100». В мультивалютных магазинах сохраняйте курс обмена в заказе и решите, конвертируете ли вы только цены (валюта отображения) или реально списываете деньги в этой валюте (валюта расчётов).
// Split an integer amount across weights; parts always sum to the total
export function allocate(total: number, weights: number[]): number[] {
const sum = weights.reduce((a, b) => a + b, 0);
const raw = weights.map((w) => (total * w) / sum);
const parts = raw.map(Math.floor);
let rest = total - parts.reduce((a, b) => a + b, 0);
const order = raw
.map((r, i) => ({ i, frac: r - Math.floor(r) }))
.sort((a, b) => b.frac - a.frac);
for (const { i } of order) {
if (rest-- <= 0) break;
parts[i] += 1;
}
return parts;
}
allocate(500, [1999, 999, 2]); // [333, 167, 0] → sums to 500
09Read models для быстрых страниц каталога
Страница категории, которая на каждый запрос джойнит товары, варианты, цены, остатки, изображения и акции, с ростом каталога становится медленной. Обычный ответ — read model: денормализованная таблица (или документ поискового индекса) с одной строкой на товар, где лежит ровно то, что нужно листингу, — минимальная цена, флаг наличия, главное изображение, значения атрибутов для фильтров, рейтинг. Фоновые задачи обновляют её при изменении исходных данных, а витрина читает только эту таблицу. Кэшируйте отрендеренные фрагменты или ответы API с тегами, чтобы изменение цены инвалидировало только затронутые товары. Write model (нормализованные таблицы, транзакции) остаётся источником истины; read model всегда можно перестроить из неё. Такое разделение, которое иногда называют CQRS (command query responsibility segregation), позволяет масштабировать чтение, не усложняя оформление заказа.
- Перестраивайте read-строку товара в задаче из очереди после изменения цены, остатка или контента.
- Для глубоких страниц используйте keyset-пагинацию (WHERE id > last_id).
- Никогда не вычисляйте живые цены по акциям для каждого товара в листинге; заранее рассчитывайте бейджи.
10Граничные случаи безопасности в корзинах и заказах
Корзины и заказы — классическая цель для insecure direct object references (IDOR): меняешь /orders/1042 на /orders/1043 и видишь чужой заказ. Используйте в URL неугадываемые публичные идентификаторы (UUID или случайные токены) и всегда ограничивайте запросы владельцем. Меняйте токен гостевой корзины при входе пользователя, чтобы токен, перехваченный до входа, нельзя было использовать для чтения корзины аккаунта. Валидируйте количество как положительное целое с верхним пределом — отрицательные количества в плохо сделанных магазинах приводили к реальным возвратам денег. Ставьте rate limit на эндпоинты купонов и оформления заказа, чтобы замедлить перебор и card testing, когда злоумышленники прогоняют украденные карты через мелкие заказы. И наконец, храните в заказах минимум персональных данных и продумайте срок их хранения, потому что таблицы заказов незаметно становятся вашим самым большим хранилищем персональных данных.
Реальные ситуации
Флеш-распродажа продаёт больше главного товара, чем есть
Проблема
За часовую распродажу продали 300 единиц, хотя существовало только 200. Остаток проверялся при добавлении в корзину и ещё раз в коде приложения прямо перед сохранением заказа.
Как решить
Проверка и списание были двумя отдельными шагами, поэтому все параллельные оформления видели, что товар есть. Замените их одним условным апдейтом — UPDATE stock_levels SET quantity = quantity - :qty WHERE variant_id = :id AND quantity >= :qty — внутри транзакции заказа и считайте ноль затронутых строк признаком «нет в наличии». Резервируйте при создании заказа, освобождайте при неудачной оплате и отменяйте неоплаченные заказы через 15 минут плановой задачей. Для следующей распродажи добавьте счётчик в Redis как быструю предварительную проверку, чтобы снять нагрузку, оставьте защиту в базе главной и перед кампанией проведите нагрузочный тест с параллельными оформлениями на staging.
На оформлении покупатели видят сумму больше, чем в корзине
Проблема
В поддержку приходят жалобы: корзина показывает 42.00, а экран оплаты просит 45.50. При этом каждый экран по отдельности работает правильно.
Как решить
Почти всегда это значит, что итоги считаются в двух местах: страница корзины использует упрощённый расчёт, а оформление по-другому применяет стоимость доставки, правила акций или налог. Перенесите весь расчёт цен в один серверный pricing-сервис, который возвращает полную разбивку — строки, скидки, доставка, налог, итог, — и пусть оба экрана рендерят этот ответ. Когда между экранами что-то законно меняется (стоимость доставки после ввода адреса, обновление цены), возвращайте явное сообщение с описанием изменения. Добавьте регрессионный тест, который прогоняет одну и ту же корзину через эндпоинт корзины и эндпоинт оформления и проверяет, что итоги совпадают.
В ужин временные слоты доставки переполняются
Проблема
Филиал ресторана может приготовить около 20 заказов за 15 минут, но на слот 19:00 регулярно приходит 60 заказов. Кухня не успевает, а опоздавшие доставки приводят к возвратам.
Как решить
Слотам нужна вместимость, а не только время. Храните вместимость для каждого филиала и слота (с настройкой по дням недели) и обращайтесь с бронированием слота как с остатком: атомарный условный инкремент booked_count, который проходит, только пока он меньше вместимости, в той же транзакции, что и создание заказа. Показывайте заполненные слоты неактивными и предлагайте ближайший свободный. Заказы «как можно скорее» должны занимать место в первом слоте после времени приготовления. Дайте менеджерам филиалов переключатель, чтобы приостановить заказы «как можно скорее» или уменьшить вместимость в час пик, и стройте отчёт по загрузке слотов, чтобы бизнес мог планировать персонал на пиковые часы.
Шпаргалка
- Атомарное списание остатка
UPDATE stock_levels SET quantity = quantity - :qty WHERE variant_id = :id AND quantity >= :qty;- Блокировка строки внутри транзакции
SELECT quantity FROM stock_levels WHERE variant_id = :id FOR UPDATE;- Идемпотентная вставка заказа (PostgreSQL)
INSERT INTO orders (checkout_key, ...) VALUES (...) ON CONFLICT (checkout_key) DO NOTHING RETURNING id;- Колонки для денег
amount BIGINT NOT NULL, currency CHAR(3) NOT NULL -- minor units + ISO 4217 code- Форматирование денег для отображения
new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(1999 / 100) // $19.99- Cookie гостевой корзины
Set-Cookie: cart_token=<random>; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000; Path=/- Найти зону доставки (PostGIS)
SELECT id FROM delivery_zones WHERE ST_Contains(area, ST_SetSRID(ST_MakePoint(:lng, :lat), 4326));- Отмена неоплаченных заказов (Laravel)
Schedule::command('orders:expire-pending')->everyFiveMinutes()->withoutOverlapping();
В моих реальных проектах
- На платформе заказов для сети ресторанов я сделал гостевые корзины со слиянием при входе и повтором заказа, определение зон доставки по геокоординатам и движок временных слотов для «как можно скорее», доставки ко времени и самовывоза — с понятием «рабочего сегодня» и округлением интервалов.
- На платформе товаров для животных в каталоге есть вложенные категории, бренды, семейства атрибутов и варианты, с режимами продажи упаковкой и на вес, а цены смоделированы как value objects; checkout использует сервис жизненного цикла заказа и может исполнять заказ со склада, через поставщика или под заказ (backorder).
- Я вынес повторяющийся код аккаунта, корзины и checkout в переиспользуемые внутренние пакеты, которые используются примерно на десяти клиентских сайтах, и намеренно не допускал попадания данных карт в сохраняемое хранилище корзины.
Вопросы и ответы
Q1Почему деньги никогда не стоит хранить во float?
Float — это двоичные приближения, поэтому значения вроде 0.1 нельзя представить точно. Мелкие ошибки появляются в суммах, скидках и разбивке налогов, и после тысяч заказов бухгалтерия перестаёт сходиться с платёжным провайдером. Я храню целые числа в минимальной единице валюты вместе с ISO-кодом валюты и использую value object для денег при вычислениях и форматировании. Округление происходит в одном месте по явному правилу, обычно по каждой строке. В базе данных подходят и integer, и DECIMAL с фиксированной точностью; главное — никогда не выполнять арифметику на float в коде приложения.
Q2Как вы сливаете гостевую корзину, когда пользователь входит в аккаунт?
Гостевая корзина — это серверная корзина, привязанная к случайному токену. При входе я загружаю обе корзины в транзакции, переношу каждую гостевую строку в корзину пользователя, суммирую количества для одного и того же варианта и ограничиваю их остатком или лимитами на заказ. Затем удаляю гостевую корзину и очищаю токен. После слияния я заново валидирую всю корзину и возвращаю предупреждения — например, что товар закончился. Слияние на сервере обеспечивает согласованность между вебом и приложением и не даёт потерять товары.
Q3Как не дать двум покупателям купить последний товар?
Изменения остатков должны быть атомарными. Внутри транзакции я либо блокирую строку остатка через SELECT FOR UPDATE, либо выполняю условное обновление, которое уменьшает количество только при достаточном остатке, и проверяю число затронутых строк. Резервирование происходит при создании заказа, а не при добавлении товара в корзину. Если оплата не прошла или заказ истёк, задача освобождает резерв. Для очень ходовых товаров доступ можно сериализовать через счётчик в Redis или очередь, но защита на уровне базы данных остаётся последней инстанцией.
Q4Зачем сохранять снимок цен в строке заказа?
Товары меняются: цены растут, названия редактируют, товары удаляют. Заказ — это юридическая и финансовая запись о том, что произошло в конкретный момент, поэтому он не должен зависеть от текущего каталога. Я копирую в каждую строку заказа название товара, атрибуты варианта, цену за единицу, скидку, налог и валюту. Счета, возвраты, отчёты и экспорт в ERP затем читают данные из самого заказа. Это ещё и упрощает возвраты, потому что точно известно, сколько было списано за каждую строку.
Q5Как технически выглядит хороший процесс оформления заказа?
Клиент отправляет токен корзины, способ получения, адрес или филиал, временной слот и способ оплаты. Сервер заново проверяет всё: доступность, остатки, цены, акции, зону доставки и слот. Если всё в порядке, он в одной транзакции создаёт заказ и резервирует остатки со статусом «ожидает оплаты», а затем запускает платёж. Заказ становится оплаченным только после проверенного подтверждения от платёжного шлюза. Письма, push-уведомления и интеграции запускаются переходами статусов через задачи в очереди, а просроченные неоплаченные заказы отменяет задача по расписанию.
Q6Должны ли цены включать налог?
Зависит от рынка. Потребительские магазины в большей части Европы, в Армении и Австралии показывают цены с налогом, а налог выделяется для счетов. Магазины в США обычно показывают цены без sales tax, который рассчитывается по адресу доставки. Главное — выбрать одно соглашение для магазина, хранить ставку и сумму налога в каждой строке заказа и применять одно правило округления. Для сложных налогов в нескольких регионах сервис вроде Stripe Tax или Avalara часто дешевле, чем поддерживать правила самостоятельно.
Вопросы уровня senior
S1Как сделать оформление заказа устойчивым к повторам, двойным отправкам и параллельным запросам от начала до конца?
Я выстраиваю три уровня защиты. Первый: клиент отправляет checkout key, сгенерированный на каждую попытку; уникальный индекс в orders означает, что повторный запрос вернёт существующий заказ, а не создаст новый. Второй: короткая блокировка на корзину сериализует параллельные отправки, так что валидация и вставка происходят один раз. Третий: каждый побочный эффект идемпотентен — резервирование остатка использует условные апдейты, вызов оплаты несёт idempotency key, производный от заказа, а письма и выгрузки в ERP идут из задач в очереди с ключом по заказу и переходу. Цена — немного дополнительного состояния и пара лишних индексов, зато это убирает целый класс инцидентов — дубли заказов и двойные списания, — которые очень дорого разгребать вручную.
S2Когда стоит выносить каталог, корзину и заказы в отдельные сервисы?
На старте — редко. Модульный монолит с чёткими модулями — каталог, цены, корзина, заказы, платежи — и одной базой даёт транзакции между ними, а от этого зависит оформление заказа. Я бы разделял при конкретном давлении: нагрузку на чтение каталога нужно масштабировать отдельно, разные команды владеют разными доменами и блокируют релизы друг друга, или у модуля совсем другие требования к доступности. Даже тогда сначала я бы выносил части с интенсивным чтением, например поиск и read models каталога, которые терпят eventual consistency. Заказы и платежи остаются вместе дольше всего, потому что их разделение заменяет транзакцию в базе на сагу с компенсирующими шагами, больше точек отказа и более тяжёлую отладку. Микросервисы решают организационное масштабирование больше, чем техническое.
S3Как подготовить магазин к большому дню распродаж?
Чтение и запись масштабируются по-разному. Трафик каталога и листингов — в основном чтение, поэтому я отдаю его из read models, кэширую ответы на CDN с короткими TTL и инвалидацией по тегам, а поиск держу на отдельном движке. Оформление заказа — это запись, и оно обязано оставаться корректным, поэтому я держу его лёгким: никакой тяжёлой работы в запросе, побочные эффекты в очередях, проверенные индексы на таблицах заказов и остатков, пул соединений под пиковую нагрузку. Для горячих товаров перед защитой остатка в базе стоит быстрая предварительная проверка в Redis. Я провожу нагрузочное тестирование реального потока оформления, настраиваю алерты на глубину очередей и уровень ошибок, замораживаю деплои на время события и готовлю feature flags, чтобы отключить второстепенные функции вроде рекомендаций, если система под давлением.
Глоссарий
- SKU
- Stock-keeping unit: уникальный код одного продаваемого варианта, используется для остатков, цен и интеграций.
- Вариант
- Конкретная версия товара с определёнными значениями атрибутов, например красный, размер M.
- Минимальная единица валюты
- Наименьшая единица валюты, например центы. ISO 4217 определяет, сколько знаков после запятой у каждой валюты.
- Снимок цены
- Копия названия, цены, скидки и налога, сохранённая в строке заказа в момент покупки.
- Резервирование
- Остаток, отложенный под оформленный заказ, чтобы никто другой не купил его до отправки или отмены.
- Checkout key
- Уникальный ID, который клиент генерирует на каждую попытку оформления; он предотвращает дубли заказов при повторах.
- Read model
- Денормализованная копия данных, приспособленная для быстрого чтения и перестраиваемая из исходных таблиц.
- IDOR
- Insecure direct object reference: доступ к чужой записи путём замены ID в URL или запросе.
- Card testing
- Мошенники проводят множество мелких платежей украденными картами, чтобы выяснить, какие из них ещё работают.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.