Ограниченные контексты для маркетплейсов: разделяйте правила заранее
Узнайте, как ограниченные контексты для маркетплейсов помогают заранее разделить правила покупателя, продавца, выплат и поддержки, чтобы одна общая схема не породила конфликтов.

Когда одна схема перестаёт работать
Одна схема для маркетплейса кажется рабочей в начале. Одна запись заказа, одно поле статуса, одна таблица пользователей. Это срабатывает, пока продукт небольшой и путь «по умолчанию» прост.
Проблемы начинаются, когда действия покупателя и продавца перестают совпадать. Покупатель хочет искать, оплачивать, отменять, возвращать и получать обновления. Продавцу нужно принимать или отклонять заказ, готовить товар, отправлять, решать проблемы со складом и отвечать на претензии. Оба касаются одной транзакции, но правила у них разные.
Поддержка увеличивает разрыв. Оформление заказа хочет чистый поток. Поддержка разбирается с повреждёнными товарами, частичными возвратами, исправлением адресов, проверками на мошенничество, потерянными посылками и ручными исключениями. Если все эти случаи живут в той же логике схемы, что и оформление, модель быстро становится шумной. Поля, которые выглядели простыми, как status или refund_reason, начинают нести слишком много значений.
Выплаты отделяются дальше. Заказ происходит, когда покупатель платит. Выплаты происходят, когда платформа считает деньги безопасными для перечисления. Это может быть через дни или дольше из‑за возвратов, резервных периодов, чарджбеков или проверок соответствия. Когда одна схема заказа пытается владеть и временем покупки, и временем выплат, команды начинают втиснуть несвязанные состояния в одну временную линию.
Небольшой пример показывает нагрузку. Покупатель заказывает мебель ручной работы. Продавец подтверждает заказ через два дня. Поддержка меняет адрес доставки после оформления. Товар приходит повреждённым, и поддержка одобряет частичный возврат. Выплата продавцу остаётся на удержании, пока претензия не закроется. Это одна история клиента, но в ней четыре разные наборы правил.
Вы обычно заметите перелом по флагам. Команды добавляют поля вроде manual_review, seller_confirmed, refund_pending, payout_hold и support_override. Потом добавляют комбинации флагов, потому что одного флага уже недостаточно. После этого никто не доверяет одной строке, чтобы объяснить, что на самом деле происходит.
Вот где ограниченные контексты перестают быть абстракцией. В маркетплейсе они помогают не дать потокам покупателя, операциям продавца, исключениям поддержки и правилам выплат конфликтовать в одной схеме.
Первые четыре набора правил, которые следует разделить
Маркетплейсы кажутся простыми в начале. Одна таблица пользователей, одна таблица заказов, одна таблица товаров. Потом возвраты влияют на балансы продавца, агенты поддержки делают исключения, и появляются налоговые поля там, где им не место.
Первое разделение обычно должно следовать за бизнес‑принадлежностью.
Правила покупателя должны быть вместе. Эта зона отвечает за поиск, корзину, оформление заказа, статус оплаты, видимый покупателю, и запросы на возврат. Она отвечает на простые вопросы: что может купить этот человек, какую цену он видит и что случится, если он отменит?
Правила продавца достаточно отличаются, чтобы отделить их рано. Продавцам важны листинги, инвентарь, комиссии, нарушения правил и споры, связанные с исполнением. Продавец может принять заказ, отклонить причину возврата или заплатить категориальную комиссию. Ничто из этого не должно менять работу корзины.
Правила выплат должны иметь свою модель, даже если деньги впервые появляются в заказах. Выплаты отслеживают удержания, сроки расчётов, отмены, налоговые детали и итоговую сумму после комиссий и возвратов. Заказ может показывать paid, в то время как выплата всё ещё говорит hold for 7 days. Оба корректны — просто в разных контекстах.
Поддержке тоже нужно отдельное «место». Агенты поддержки открывают кейсы, добавляют заметки, собирают доказательства и принимают ручные решения. Они могут одобрить исключение, которое обычные продуктовые правила бы отклонили. Это не значит, что поддержка должна владеть комиссиями продавцов, расчётами выплат или логикой оформления заказа.
Простая карта ответственности помогает больше, чем ещё одна правка схемы:
- Покупатель владеет потоком покупки и видимыми покупателю возвратами.
- Продавец владеет листингами, запасами, комиссиями и ответами на споры по исполнению.
- Выплата владеет удержаниями, расчётами, отменами и налоговыми записями.
- Поддержка владеет кейсами, заметками и ручными переопределениями.
Запишите это прежде, чем трогать таблицы. Если приходит возврат, сторона покупателя может принять запрос, поддержка проверить крайние случаи, правила продавца обработать спор, а выплаты — скорректировать расчёт. Это куда легче понимать, чем одна таблица заказов, пытающаяся притвориться корзиной, бухгалтерской книгой, порталом продавца и службой поддержки одновременно.
Как найти реальную границу
Реальная граница обычно проявляется до того, как код станет ужасен. Вы это чувствуете, когда одна часть продукта меняется каждую неделю, а другая часть должна оставаться стабильной.
Начните с владельцев. Спросите, кто чаще просит изменения правил. Если команда поддержки постоянно меняет правила по кейсам, а правила выплат движутся только когда просит финанс, эти части, вероятно, не должны жить в одной модели. Общая схема делает каждое мелкое изменение рискованным, потому что каждая команда тянет одну и ту же запись в разную сторону.
Слова — ещё одна подсказка. Если один и тот же термин значит разное для разных людей, у вас, скорее всего, уже два контекста. Order — классический пример. Покупатель думает об order как об одной покупке. Продавец может воспринимать его как одну отгрузку или одну позицию. Сторона выплат заботится о моменте, когда деньги проходят. Поддержка может трактовать тот же order как переписку со статусом, заметками и вложениями.
Изменения состояний говорят то же самое. Покупатель‑ориентированный заказ может идти от pending до paid до delivered. Запись выплат может идти от pending до on_hold до released. Кейсы поддержки следуют совсем другой схеме. Когда одна таблица пытается держать все эти состояния, команды начинают добавлять флаги, чтобы объяснить исключения, и модель становится менее надёжной.
Несколько проверок работают на практике:
- Одна группа меняет правила гораздо чаще, чем другая.
- Одно слово требует разных определений на совещаниях.
- Одна запись проходит через разные состояния в зависимости от того, кто её использует.
- Маленькое изменение ломает тесты или рабочие процессы в другой области.
Последний пункт важнее, чем многие ожидают. Если обновление политики продавца ломает письма покупателям, или заметка поддержки меняет поведение выплат — граница уже существует. Код просто ещё не успел за этим.
Есть и простой тест: можете ли вы объяснить правило, не упоминая заботы другой команды? Если вы можете описать правила принятия продавцом заказа без обсуждения обработки возвратов, или триаж поддержки без упоминания сроков выпуска выплат, — разделите ответственность там. Сохраняйте общие идентификаторы при необходимости, но не заставляйте одну схему владеть каждым смыслом.
Практичный способ разделить модель
Начните с малого и будьте буквальны. Напишите одну короткую пользовательскую историю для каждой стороны: покупатель делает заказ и просит возврат; продавец принимает, упаковывает и отправляет; выплаты перечисляют деньги после прохождения проверок; поддержка вмешивается, когда что‑то идёт не так. Если для одной истории нужен другой набор решений — ей, вероятно, нужна своя модель.
Затем перечислите состояния, через которые проходит каждая сторона. У заказа покупателя это может быть: корзина → оплачено → доставлено → запрос на возврат. У отгрузки продавца свой путь: принято → упаковано → отправлено → задержано → отменено. У выплат и поддержки тоже должны быть отдельные списки состояний, даже если они касаются одного и того же заказа.
Когда состояния видны, поместите каждое правило туда, где принимается решение. Может ли покупатель отменить после отправки? — принадлежит покупателю и правилам заказа. Может ли продавец объединить две отгрузки? — принадлежит исполнению продавца. Когда деньги перечислять? — принадлежит выплатам. Кто может одобрить исключение? — принадлежит поддержке.
Если две группы интересуются одним и тем же фактом, одна из групп всё равно должна владеть решением и публиковать результат.
Небольшой пример упрощает задачу. Если покупатель открывает возврат, поддержка может одобрить исключение, но выплаты не должны читать весь кейс поддержки и догадываться, что произошло. Поддержка может отправить простое событие return_approved с ID заказа и кодом причины. Выплаты прочитают это событие и применят собственные правила для удержания, частичного выпуска или отмены.
Держите общие данные скучными и стабильными. ID, метки времени, суммы и несколько базовых статусов можно разделять или копировать по необходимости. Правила политик, заметки, внутренние флаги и детали рабочих процессов должны оставаться внутри группы, которая ими пользуется.
Хорошее разделение обычно простое: каждая группа владеет своими состояниями и правилами, общие записи остаются тонкими, другие группы читают ID и факты, и никто не правит приватными полями другой группы.
Сначала это кажется медленнее. Позже это экономит огромное количество уборки.
Пример маркетплейса с возвратами и выплатами
Запрос на возврат показывает, почему одна общая схема быстро становится грязной. Покупатель получает товар в понедельник и просит возврат во вторник. Заказ всё ещё показывает delivered, но этот статус не может нести правила поддержки, доказательств продавца и движения денег.
Поддержка открывает кейс в собственной модели. В кейсе есть причина, статус, дедлайны, сообщения и доказательства. Заказу всё это не нужно, а записи выплат уж точно не нужны.
Одновременно поток выплат ставит перевод продавца на удержание. Это правило выплат, а не правило заказа. Покупатель всё ещё имеет завершённую покупку, продавец — отгруженный заказ. Поддержка меняет, выплаты реагируют.
Продавец отвечает внутри кейса. Он может добавить трекинг, фото доставки или подписанный чек. Поддержке нужны эти доказательства для решения, а выплатам нужен лишь один факт: удерживать деньги, выпустить их или отменить.
Это разделение можно смоделировать без драмы:
- Данные покупателя покрывают заказ, позиции, запрос на возврат и сообщения, которые видит покупатель.
- Данные продавца покрывают детали отгрузки, доказательства доставки и историю ответов.
- Данные поддержки покрывают статус кейса, доказательства, внутренние заметки и дедлайны.
- Данные выплат покрывают состояние удержания, суммы, бухгалтерские проводки и результат — выпуск или отмена.
Когда поддержка принимает решение, выплаты действуют по этому результату. Если поддержка одобряет возврат, поток выплат отменяет удержание и фиксирует результат возврата. Если поддержка отклоняет претензию, выплаты освобождают деньги продавцу.
Это ещё важнее, когда правила становятся более специфичными. Частичные возвраты, претензии по повреждению или задержки выплат для рисковых заказов кажутся управляемыми по отдельности. Поместите их в одну таблицу заказов — и каждое новое правило превращается в ещё один флаг, ещё одно исключение и ещё один ночной баг. Держите факты каждой группы локальными и передавайте только сигналы, которые нужны следующей группе.
Данные, которые кажутся общими, но не должны быть
Команды часто хранят всё, что кажется общим, в одной таблице. Маркетплейс этому подталкивает: один и тот же человек может покупать, продавать, обращаться в поддержку и получать деньги, но каждое действие подчиняется разным правилам.
Одно поле статуса — обычно первая ловушка. Active может означать, что покупатель может делать заказы, продавец может публиковать товары, выплата может двигаться или кейс поддержки всё ещё открыт. Это не одно и то же состояние. Когда одна колонка пытается покрыть всё, команда начинает добавлять флаги и исключения, пока данные не скажут меньше, а не больше.
Та же проблема появляется с пользователями. Один аккаунт — не значит одна модель пользователя. Покупателю нужны адреса, возвраты, сохранённые способы оплаты и история заказов. Продавцу нужны налоговые данные, настройки магазина, история модерации и настройки выплат. Один и тот же человек может иметь обе роли, но бизнес‑правила всё равно живут в разных местах. Общая идентичность — не общее значение.
Заметки поддержки — ещё одна частая утечка. Если агент пишет «продавец согласился заменить товар после проверки фото», эта заметка принадлежит кейсу поддержки. Она не принадлежит записи продавца. Заметки по кейсу грязные, временные и привязаны к конкретному спору. Если вы сохраняете их в таблице продавцов, другая команда может позже прочесть их как постоянный факт.
Данные выплат нуждаются в той же границе. Таблица выплат должна отвечать на финансовые вопросы: сколько заработал продавец, какие комиссии применялись, какая сумма на удержании и когда деньги могут уйти. Она не должна решать, нужно ли одобрить листинг, есть ли у категории особые правила или изменилось ли окно возврата. Финансовые записи должны фиксировать результат этих правил, но не владеть ими.
Когда данные кажутся общими, задайте простые вопросы. Читает ли поле две команды для разных причин? Смешивается ли временная деталь кейса с долгосрочными данными профиля? Хранит ли запись выплат поведение каталога вместо сумм и дат? Повторно используйте идентичность там, где это помогает, но держите правила, завязанные на ролях, отдельно.
Ошибки, которые создают переработку
Команды обычно создают переработку, когда считают каждое действие маркетплейса частью одного большого заказа. Сначала кажется быстрее: пара колонок, ещё несколько флагов, ещё один статус. Через полгода никто не может менять логику возвратов, не ломая выплаты или отчётность продавца.
Одна распространённая ошибка — использовать одно поле статуса для трёх разных вещей: заказа покупателя, кейса поддержки и выплаты. Эти потоки идут с разной скоростью и по разным правилам. Заказ может быть доставлен, кейс поддержки ещё открыт, а выплата — на удержании. Если одно поле пытается сказать всё это, люди начинают придумывать значения вроде completed_pending_review или paid_except_dispute.
Доступ поддержки создаёт другую проблему. Если агенты поддержки могут прямо редактировать настройки продавца или данные выплат, они решат один тикет и одновременно создадут два бухгалтерских вопроса. Поддержке достаточно записать запрос, одобрить действие или запустить рабочий процесс. Ей не нужно вручную переписывать владельцев выплат, банковские реквизиты или поля политик продавца.
Огромная таблица заказов — ещё одна ловушка. Команды продолжают наваливать исключения в неё, потому что заказ кажется общим для всех. Вскоре она хранит заметки по спорам, повторы выплат, исключения продавцов, причины ручной проверки, налоговые поля и внутренние комментарии. В этот момент таблица перестаёт описывать заказ — она становится свалкой.
Копирование каждого поля в каждую службу создаёт медленный ущерб. Одна команда копирует имя продавца, статус выплат, причину кейса и разбивку комиссии «на всякий случай». Потом данные расходятся. Поддержка видит одно значение, финансы — другое, и никто не знает, где правда.
Есть и маленькая ошибка, которая всё равно стоит времени: переименование без изменения владения. Переименование merchant_status в seller_health ничего не даёт, если та же команда, таблица и путь редактирования всё ещё смешивают онбординг, комплаенс и правила выплат.
Если для изменения одного поля нужно согласие от разных команд с разными целями, это поле, скорее всего, лежит не в том месте. Разделите владение до того, как добавите ещё один флаг.
Быстрая проверка перед добавлением ещё одного флага
Новый флаг часто кажется дешёвым. Добавьте is_paused, payout_hold или needs_review — и релиз проходит. Через пару месяцев одна запись заказа несёт шаги покупателя, обещания продавца, тайминг выплат и исключения поддержки одновременно.
Проблема обычно не в самом флаге. Проблема в том, что флаг протаскивает целый набор правил в модель, которая уже делает слишком много.
Прежде чем добавить ещё одно поле в общую таблицу, остановитесь и спросите:
- Может ли одна команда менять это правило самостоятельно?
- Управляет ли эта область собственными состояниями?
- Нужны ли другим частям полный набор данных или достаточно ID и пары фактов?
- Может ли поддержка поставить кейс на паузу без переписывания оформления заказа?
- Вынудит ли новое правило выплат менять схему покупателя?
Короткий пример делает это конкретным. Предположим, финансы хочет удерживать выплаты новым продавцам 7 дней. Это принадлежит правилам выплат. Покупатель по‑прежнему видит тот же статус покупки. Поддержка продолжает отслеживать кейсы в своём потоке. Если удержание выплат добавляет колонки в основную машину состояний заказа, граница уже размыта.
Раздельные состояния — часто самый чистый тест. Когда две области движутся с разной скоростью, позвольте каждой хранить свои имена состояний и логику. Передавайте ID и пару фактов между ними. Если новый флаг остаётся локальным — держите его локальным. Если он меняет смысл одной и той же записи для трёх команд — разделяйте модель.
Что делать дальше
Начните с одного рабочего потока, который уже вызывает путаницу. Пропишите его на бумаге. Возврат с частичным возвратом и задержанной выплатой продавцу — хороший пример, потому что команды часто смешивают обещания покупателю, правила продавца, тайминг денег и исключения поддержки в одном потоке.
Напишите каждый шаг простыми словами. Кто принимает решение? Кто владеет правилом? Какие данные нужны в этот момент? Если один блок требует фактов от трёх команд, чтобы решить простой результат, модель уже смешивает границы.
Выберите одну границу, чтобы разделить её в следующем спринте. Только одну. Выплаты часто — хороший первый шаг, потому что правила денег обычно остаются строгими, а правила покупателя и поддержки меняются чаще. Если выплаты слишком рискованны, поддержка может быть безопасным первым разделением, потому что ей нужны собственные заметки, переопределения и история кейсов.
Делайте изменение узким. Промапьте текущий рабочий процесс шаг за шагом, отметьте каждое решение как покупательское, продавческое, выплатное или поддержочное, перенесите один набор правил под одного ясного владельца, держите старые поля читаемыми до тех пор, пока отчёты не привыкнут, и проверьте реальные крайние случаи после релиза.
Не ломайте отчётность во время смены владения. Сохраните дашборды стабильными с помощью простого слоя отчётности, экспортной задачи или read‑модели, которая по‑прежнему выдаёт те же числа для финансов и операций. Команды нервничают, когда итоги меняются в процессе миграции, даже если поведение продукта становится лучше.
Если ваша модель уже разрослась от флагов, исключений и общих таблиц, полезно получить второе мнение прежде, чем резать её. Oleg Sotnikov at oleg.is работает со стартапами как фракционный CTO и часто такого рода обзор архитектуры продукта помогает увидеть рискованные соединения до того, как они превратятся в болезненный рефакторинг.
Следующий шаг редко означает полный ребилд. Это одна чистая граница, один запутанный рабочий процесс, хорошо задокументированный, и один релиз, который облегчает следующие изменения.
Часто задаваемые вопросы
Когда одна схема заказов перестаёт быть достаточной?
Одна схема перестаёт работать, когда одна запись пытается одновременно описать оформление покупки, исполнение продавцом, кейсы поддержки и тайминг выплат. Если команда постоянно добавляет флаги, чтобы объяснить обычные операции — разделяйте модель.
Какой контекст разделять в первую очередь?
Начните с области, которая создаёт наибольшее трение. Чаще всего это выплаты — правила по деньгам более стабильны, тогда как поддержка приносит много исключений и заметок, которые протекают в заказы.
Как заметить реальную границу до того, как код станет хуже?
Смотрите, кто чаще всего меняет правила, и как люди говорят об одной и той же записи. Если поддержка, продавцы и финансы по-разному понимают слово order, или одно маленькое изменение ломает другую область — у вас уже разные контексты.
Может ли один человек быть и покупателем, и продавцом без одной гигантской модели пользователя?
Да. Разделяйте вход в систему и идентичность, а затем храните данные покупателя и продавца в отдельных моделях. Одна и та же людина может выполнять обе роли, но адреса и возвраты не должны храниться рядом с налоговыми данными и настройками выплат.
Где должны храниться возвраты?
Пусть сторона покупателя владеет запросом на возврат, который видит клиент. Доказательства, заметки по кейсу и обработку исключений храните в поддержке, а выплаты — владеют финансовым результатом.
Какие данные должны быть общими между контекстами?
Делитесь простыми фактами: идентификаторами, временными метками, суммами и ясными кодами результатов. Заметки, правила политик, внутренние флаги и детали рабочего процесса оставляйте там, где принимают решение.
Держать одну колонку статуса для заказов, поддержки и выплат?
Нет. Дайте каждому контексту свои состояния. Заказ может быть delivered, поддержка — open, а выплата — on_hold — и это не конфликт.
Как поддержка должна сообщать выплатам о результате?
Поддержка должна отправлять небольшой очевидный результат, например return_approved с ID заказа и кодом причины. Платёжная логика затем решает, удерживать, выпустить или отменить средства по своим правилам.
Означают ли ограниченные контексты, что нужно сразу переходить на микросервисы?
Нет. Можно сначала разделить модели и ответственность внутри одного приложения. Сервисы могут появиться позже; важнее ясные границы, а не форма деплоя.
Нужен ли полный рефакторинг, чтобы исправить запутанную схему маркетплейса?
Нет. Начните с одного болезненного потока: пропишите шаги, отметьте хозяина каждого решения и перемещайте одну границу за релиз. Поддерживайте старые поля читаемыми до тех пор, пока отчёты не привыкнут.