Как говорить об ограничениях, не оправдываясь
Сильный сигнал на собесе — не отсутствие ограничений (у любого MVP они есть), а то, называет ли кандидат их сам, до вопроса, и может ли сразу сказать, что именно изменится при росте нагрузки/данных. Ниже — реестр осознанных ограничений «Avito.Кухни» с готовой формулировкой для каждого.
Заказы, зависшие в confirmed, не освобождают остаток автоматически
Если outbox не может опубликовать сообщение длительное время (например, Kafka недоступна), заказ остаётся в confirmed, а списанный при создании остаток не возвращается — товар эффективно «заморожен» на складе. Не решается автоматически: TTL/авто-отмена зависших заказов — отдельный механизм (нужен scheduler, политика "сколько ждать", уведомление клиента), избыточный для MVP.
Формулировка: «Известное ограничение, зафиксировано явно, а не забыто. В проде это закрывается фоновым job'ом с политикой авто-отмены по TTL — не реализовано, потому что для объёма демо-данных это никогда не наблюдается, и я предпочёл потратить время на защиту от конкурентных гонок (пункт, который реально проверяется прогоном), а не на TTL-политику, которую пришлось бы придумывать без реальных данных о типичном времени доставки».
GET /orders/{id} не проверяет владение заказом
Любой вызывающий с известным order_id может прочитать чужой заказ — UUID делает перебор практически невозможным, но это не замена настоящей проверке владения.
Формулировка: «Сквозная авторизация — по условию задания зона ответственности платформы выше уровня этого сервиса (пользовательская аутентификация explicitly не требуется). Если бы её нужно было делать здесь — проверка владения добавляется одной строкой в usecase (order.UserID != requestingUserID → 404, не 403, чтобы не подтверждать сам факт существования чужого заказа перебором)».
Пагинация offset-based, не cursor-based
limit/offset на всех списочных ручках. Работает предсказуемо для объёма демо-данных, деградирует на очень больших офсетах (Postgres всё равно сканирует и отбрасывает первые N строк).
SELECT * FROM orders
WHERE user_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3
-- страница 500 при limit=20 -> Postgres
-- сканирует и отбрасывает 9980 строк
-- перед тем как вернуть нужные 20SELECT * FROM orders
WHERE user_id = $1 AND created_at < $2
ORDER BY created_at DESC
LIMIT $3
-- $2 - created_at последней строки
-- предыдущей страницы: индекс сразу
-- прыгает к нужной позиции без сканирования
-- пропущенных строк - но нельзя запросить
-- "страницу 500" напрямую, только "следующую"Формулировка: «Для MVP offset проще и достаточен. Если бы список рос на порядки — переход на <span class="mbl-term" data-term="keyset-пагинация" data-def="Она же cursor-based: следующая страница запрашивается не по номеру (OFFSET), а по значению последней увиденной строки (WHERE created_at < $cursor) - индекс сразу прыгает к нужной позиции, не сканируя и не отбрасывая пропущенные строки.">keyset-пагинацию убирает деградацию, но ломает прямой переход "на страницу N" — trade-off, который сейчас не оправдан объёмом данных».
Поиск товаров — ILIKE, без полнотекстового индекса
GET /items?q= — ILIKE '%q%' по имени, без pg_trgm. Обычный btree-индекс на items.name не ускоряет ILIKE с ведущим % — для честного ускорения нужен pg_trgm GIN-индекс.
Формулировка: «Известно и явно отмечено — ILIKE с ведущим wildcard не пользуется обычным индексом. Для прод-объёма данных нужен CREATE INDEX ... USING GIN (name gin_trgm_ops) или полноценный full-text search — не сделано, потому что для объёма демо-данных (единицы позиций меню) разница не наблюдаема, а тащить pg_trgm ради непроверяемого на этом объёме выигрыша — не оправданная сложность».
Демо-ключи фиксированы, не генерируются
ESTABLISHMENT_API_KEY/PUSH_SECRET-аналоги для единственного демо-заведения — статичные значения в .env.example, не сгенерированы случайно при сидинге.
Формулировка: «Осознанное упрощение демо-стенда — ключ должен быть заранее известен человеку, который поднимает проект и хочет проверить сценарии руками, без похода в логи миграции за сгенерированным значением. В проде — реальная генерация + одноразовая выдача через отдельный защищённый канал, не через открытый .env.example в репозитории».
Асимметрия: outbox есть только у kitchen-service
Establishment-service публикует статус синхронно из /advance, без transactional outbox.
Формулировка (не ограничение, а осознанная асимметрия — стоит уметь отличать одно от другого перед интервьюером): «Это не забыли сделать одинаково — /advance человеко-инициируемое демо-действие, и повторный вызов с тем же action естественно повторяет попытку публикации при сбое (см. идемпотентный /advance). Реальный источник потери сообщения (падение процесса между записью в БД и публикацией) актуален там, где событие рождается автоматически без участия человека, готового нажать кнопку ещё раз — то есть на стороне kitchen, где заказ создаётся клиентским запросом, а не в establishment-service».
Общий приём для собеса
Каждое ограничение выше подаётся по одной и той же трёхчастной формуле: что именно не сделано → почему это не критично для текущего объёма/условий → что конкретно потребовалось бы изменить, если бы условие перестало выполняться. Третья часть — самая важная и чаще всего пропускается неуверенными кандидатами: назвать общие слова «можно оптимизировать» вместо конкретного pg_trgm-индекса или конкретного WithinTx с TTL-джобом — ровно то расхождение, которое интервьюер прицельно ищет вопросом «а что будет при росте нагрузки».