РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ НА MIDDLE FRONTEND РАЗРАБОТЧИКА (СУПЕР ЛЕГКИЙ СОБЕС)
Сегодня мы разберем настоящее собеседование на позицию Middle Frontend-разработчика в стартап за 140 тысяч рублей, которое прошло в двух этапах: техническом с командой и управленческом с фаундером. Интервью получилось необычно живым и практичным — без алгоритмов и академической теории, зато с глубоким погружением в архитектуру (FSD, Next.js), стейт-менеджмент, реальные боли соло-разработки и честный разговор о деньгах, рисках стартапа и контрактах ГПХ. Особенно ценно — прозрачность сторон: кандидат открыто назвал свои пробелы (веб-сокеты, микрофронтенды) и конкуренты (Сбер, Яндекс), а основатель — финансовую модель, раан и условия оффера.
Вопрос 1. По какому таймзону вы работаете?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:00:54"/>
Ответ собеседника: неполный. Указал смещение минус один час от Москвы, но не назвал конкретный таймзон (например, UTC+2 или Europe/Kaliningrad).
Правильный ответ:
Правильный ответ должен содержать IANA-идентификатор таймзоны (например, Europe/Kaliningrad, Europe/Moscow, Asia/Vladivostok) или смещение от UTC в формате UTC±HH:MM (например, UTC+02:00, UTC+03:00).
Почему этого недостаточно:
- «Минус один час от Москвы» — это относительная величина, а не абсолютная. Москва живет в
Europe/Moscow(постоянноUTC+03:00с 2014 года). Относительные ссылки на «Москву» создают двусмысленность: речь идет о текущем смещении или о географической зоне? - В системах логирования, мониторинга, расписания задач (cron, Kubernetes CronJob) и базах данных обязательно используется IANA Time Zone Database (tz database).
Как отвечать правильно (примеры):
Europe/Kaliningrad(этоUTC+02:00круглый год, нет перехода на летнее/зимнее время).Europe/Moscow(UTC+03:00).Asia/Yekaterinburg(UTC+05:00).UTC+02:00(если нет привязки к географическому региону, но это менее предпочтительно для серверов).
Best Practices для Senior/Tech Lead уровня:
1. Настройка окружения (Linux/Container)
В Docker-образах и на серверах таймзона задается через переменную окружения TZ или файл /etc/localtime.
# Dockerfile
ENV TZ=Europe/Kaliningrad
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
2. Работа во Go (time.LoadLocation)
Никогда не хардкодь смещения в секундах/часах. Используй time.LoadLocation с IANA-именем. Это автоматически учитывает исторические изменения правил и переходы (если они есть в зоне).
package main
import (
"fmt"
"time"
)
func main() {
// Правильно: загрузка локации по IANA имени
loc, err := time.LoadLocation("Europe/Kaliningrad")
if err != nil {
panic(err) // Ошибка, если tzdata нет в системе (например, в scratch-образах)
}
now := time.Now().In(loc)
fmt.Printf("Текущее время в зоне %s: %s\n", loc.String(), now.Format(time.RFC3339))
// Вывод: Текущее время в зоне Europe/Kaliningrad: 2023-10-27T15:30:00+02:00
}
Важно: В Go 1.15+ таймзоны встроены в рантайм (файл zoneinfo.zip), но в минимальных контейнерах (scratch, distroless, alpine без пакета tzdata) этот файл может отсутствовать. Нужно либо ставить пакет tzdata (apk add tzdata), либо использовать Go 1.20+ с встроенной зоной Local (но лучше явно указывать TZ).
3. Базы данных (PostgreSQL / MySQL)
- PostgreSQL: Храни
TIMESTAMP WITH TIME ZONE(timestamptz). Входные данные конвертируются в UTC, при выборке — вTimeZoneсессии (SET TIME ZONE 'Europe/Kaliningrad';). Никогда не используйtimestamp without time zoneдля моментов времени. - MySQL:
TIMESTAMPхранится в UTC (конвертируется из/в системную зону).DATETIME— «наивное» время без зоны. Для корректной работы репликации и кластеров настройsystem_time_zoneиtime_zoneна сервере в явный IANA-именованный формат.
4. Распределенные системы и логирование
- Единый стандарт: Все сервисы, логи, метрики, трейсы (OpenTelemetry) — только в UTC.
- Таймзона пользователя (
Europe/Kaliningrad) применяется только на границе представления (UI, отчеты, уведомления). - В протоколах (gRPC, REST) передавай время в RFC3339 / ISO 8601 с явным смещением:
2023-10-27T12:30:00Z(UTC) или2023-10-27T15:30:00+02:00(Kaliningrad).
Резюме: На интервью называй IANA-имя зоны (Europe/Kaliningrad). Это демонстрирует понимание того, как работает инфраструктура, базы данных и стандартные библиотеки языков программирования.
Вопрос 2. Какие архитектуры вы используете в React-приложениях и как организуете структуру проекта?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:01:46"/>
Ответ собеседника: правильный. Основной опыт — Feature-Sliced Design (FSD), структура строится вокруг бизнес-фич; ранее использовал атомарную архитектуру.
Правильный ответ:
Feature-Sliced Design (FSD) — это методологическая архитектура для фронтенд-проектов, нацеленная на масштабируемость, явную изоляцию бизнес-логики и контроль зависимостей. В отличие от «папок по типу» (components, hooks, utils) или чисто атомарного подхода, FSD организует код по слоям (layers), срезам (slices) и сегментам (segments), строго регламентируя направление импортов.
1. Иерархия слоев (сверху вниз — от высокоуровневого к низкоуровневому)
Импорты разрешены только вниз по списку (слой app может импортировать из pages, widgets, features, entities, shared; shared не импортирует ни откуда).
app — Инициализация приложения. Провайдеры (Redux, Router, Theme, QueryClient), глобальные стили, корневой лейаут, точка входа.
pages — Страницы маршрутизации. Композиция виджетов, сущностей и фич в конкретный URL. Не содержат бизнес-логики, только композицию.
widgets — Крупные самодостаточные блоки UI (Header, Sidebar, Feed, Modal). Могут использовать entities, features, shared. Имеют свой стейт (UI-состояние виджета).
features — Ядро бизнеса. Действия пользователя, приносящие бизнес-ценность: «Авторизация», «Добавление в корзину», «Фильтрация списка», «Лайк поста». Содержат UI (кнопки, формы) + логику (actions, api, model). Не знают о виджетах и страницах.
entities — Бизнес-сущности: User, Product, Post, Order. UI-компоненты для отображения (Card, ListItem), типы (TypeScript интерфейсы), API для работы с сущностью, стора (сlices/selectors). Не знают о фичах.
shared — Переиспользуемое «пылесос»: UI-кит (Button, Input, Modal), хуки (useDebounce, useLocalStorage), утилиты (date, format), конфиг, стили, иконки. Не знает о бизнес-домене.
2. Внутри слайса (Slice Structure) — Сегменты
Каждый слайс (папка в features, entities, widgets) имеет стандартизированную внутреннюю структуру (сегменты). Порядок импортов внутри слайса также важен (например, ui не импортирует model):
- api — Запросы к бэкенду (RTK Query, React Query, axios-инстансы), эндпоинты.
- model — Бизнес-логика и состояние: сторы (Redux slices, Zustand, Effector stores), схемы валидации (Zod/Yup), типы, селекторы, действия (actions/thunks).
- ui — React-компоненты, хуки только для UI (useToggle, useModal), стили.
- lib — Вспомогательные функции, уникальные для слайса (мапперы, парсеры).
- config — Константы, настройки слайса (лимиты пагинации, дефолтные значения).
- index.ts (Public API) — Единственная точка входа. Экспортируется только то, что нужно наружу. Скрывает детали реализации.
3. Ключевые принципы (Hard Constraints)
Public API (index.ts)
Никогда не импортируй внутренности слайса (features/auth/ui/LoginForm, entities/user/model/selectors).
// features/auth/index.ts — ПРАВИЛЬНО
export { LoginForm } from './ui/LoginForm';
export { useAuth } from './model/hooks/useAuth';
export { loginAction } from './model/slice';
// В другом слайсе — ПРАВИЛЬНО
import { loginAction, LoginForm } from 'features/auth';
// НЕПРАВИЛЬНО (нарушение инкапсуляции)
import { loginAction } from 'features/auth/model/slice';
Правило импортов (Dependency Rule)
Слой features не может импортировать widgets или pages.
Слой entities не может импортировать features.
shared — изолирован полностью.
Это предотвращает циклические зависимости и «спагетти-код».
Колокация (Colocation)
Код, который меняется вместе, лежит рядом. Хук useAddToCart, используемый только в фиче add-to-cart, лежит в features/add-to-cart/ui или model/hooks, а не в shared/hooks.
4. Отличие от Атомарной архитектуры (Atomic Design)
| Характеристика | Atomic Design | Feature-Sliced Design |
|---|---|---|
| Фокус | UI-компоненты (атомы, молекулы, организмы) | Бизнес-процессы и домен (фичи, энтити) |
| Масштабирование | Сложно: организмы растут, границы размываются | Четко: новая фича = новый слайс в features |
| Логика | Размазана по компонентам или вынесена в отдельные папки (hooks, store) | Инкапсулирована внутри слайса (model) |
| Рефакторинг | Риск задеть не связанные страницы | Безопасен: публичный API защищает потребителей |
| Применимость | UI-киты, дизайн-системы (shared/ui) | Бизнес-приложения (SaaS, E-commerce, Dashboards) |
Примечание: Атомарный дизайн отлично работает внутри shared/ui для построения дизайн-системы, но не как топ-уровневая архитектура приложения.
5. Пример структуры проекта (E-commerce)
src/
├── app/ # Слой app
│ ├── providers/ # Redux, React Query, Router, Theme
│ ├── styles/ # Глобальные CSS, reset, variables
│ └── App.tsx
│
├── pages/ # Слой pages
│ ├── product-list/
│ ├── product-detail/
│ └── cart/
│
├── widgets/ # Слой widgets
│ ├── header/
│ ├── product-card/ # UI карточки (использует entities/product)
│ ├── cart-sidebar/
│ └── filter-panel/
│
├── features/ # Слой features (БИЗНЕС-ЛОГИКА ЗДЕСЬ)
│ ├── add-to-cart/ # Фича: "Добавить в корзину"
│ │ ├── api/ # mutation addToCart
│ │ ├── model/ # slice, selectors, hooks (useAddToCart)
│ │ ├── ui/ # Button "Add to cart", Toast notification
│ │ └── index.ts # export { useAddToCart, AddToCartButton }
│ │
│ ├── auth-by-phone/
│ └── apply-promo-code/
│
├── entities/ # Слой entities
│ ├── product/
│ │ ├── api/ # getProducts, getProductById
│ │ ├── model/ # types.ts, slice.ts (productsSlice), selectors.ts
│ │ ├── ui/ # ProductCard, ProductSkeleton, ProductGallery
│ │ └── index.ts
│ │
│ ├── user/
│ └── cart/
│
└── shared/ # Слой shared
├── ui/ # Дизайн-система (Button, Input, Modal, Tooltip) -> Атомарная структура здесь уместна
│ ├── atoms/
│ ├── molecules/
│ └── organisms/
├── lib/ # utils (clsx, date-fns wrappers, classnames)
├── hooks/ # useDebounce, useMediaQuery, useEventListener
├── config/ # env, api-base-url, constants
└── styles/ # theme, mixins, global tokens
6. Чек-лист для Code Review (Senior/Tech Lead взгляд)
- Есть ли
index.tsу каждого слайса? Нет — архитектура нарушена. - Импорты идут только вниз по слоям?
featuresимпортируетentities? Да.entitiesимпортируетfeatures? Нет, ошибка. - Нет ли бизнес-логики в
widgets/pages? Виджет не должен знать, как авторизоваться, он вызываетfeatures/auth. - Не раздут ли
shared? Если хук используется только в одной фиче — он там и живёт. - Типизация: Типы сущностей (
entities/product/model/types.ts) экспортируются через Public API и используются вfeatures,widgets. Дублирование интерфейсов — признак проблемы.
Резюме: FSD — это не просто структура папок, это контракт команды. Он заставляет думать в терминах домена (Features/Entities), а не технологий (Components/Hooks), и даёт инструменты (Public API, Layer Rules) для поддержания этого контракта при росте кодовой базы до сотен слайсов.
Вопрос 3. Как выбираете архитектуру для нового проекта?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:02:20"/>
Ответ собеседника: правильный. Зависит от сложности: простые проекты не требуют FSD, для крупных с глубокой логикой — да; также учитывает предпочтения команды.
Правильный ответ:
Выбор архитектуры — это инженерное решение, балансирующее между стоимостью владения (maintenance cost), скоростью доставки (time-to-market) и когнитивной нагрузкой на команду. Не существует «лучшей» архитектуры, есть подходящая под контекст.
1. Фреймворк принятия решения (Decision Matrix)
Оцениваю проект по ключевым осям перед выбором:
| Ось | Вопросы-маркеры | Влияние на выбор |
|---|---|---|
| Жизненный цикл | MVP на 3 месяца / Продукт на 3-5 лет / Внутренний инструмент? | MVP → проще структура (flat / grouping by feature). Долгий продукт → инвестиции в модульность (FSD, Modular Monolith, Clean Architecture). |
| Команда | 2-3 человека / 5+ фуллстек / Распределенные команды / Джуниоры? | Маленькая команда → overhead от FSD/Modular не окупится. Большие/растущие команды → нужны жесткие границы (Public API, Layer Rules) для параллельной работы. |
| Доменная сложность | CRUD-админка / Сложные бизнес-процессы (финтех, логистика, real-time коллаборация)? | Анемичная модель (CRUD) → слоистая архитектура (Controllers/Services/Repositories). Богатая доменная модель → DDD (Domain-Driven Design), Tactical Patterns (Aggregates, Domain Events). |
| Технические ограничения | Микросервисы / Монолит / Serverless / Edge? | Микросервисы → архитектура внутри сервиса проще, сложность на уровне контрактов. Модульный монолит → четкое разделение модулей (Modulith). |
| Частота изменений | Стабильные требования / Постоянные пивоты / A/B эксперименты? | Высокая частота → нужна изоляция изменений (Feature Flags, Plugin Architecture, FSD Features). |
2. Спектр архитектурных стилей (от простого к сложному)
Уровень 1: Flat / Grouping by Route/Feature (Для MVP, маленьких команд, админок) Структура по страницам или фичам без строгой иерархии слоев.
src/
├── features/ # или pages/
│ ├── auth/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── api.ts
│ │ └── index.ts
│ └── dashboard/
├── shared/ # только UI-кит, утилиты, конфиг
└── app/
Плюсы: Низкий порог входа, быстро пишется, легко рефакторить. Минусы: При росте возникает спагетти, неявные зависимости, дублирование логики.
Уровень 2: Feature-Sliced Design (FSD) / Modular Architecture (Стандарт для продуктовых команд 5+ чел) Жесткая иерархия слоев, Public API, правило зависимостей. Описано в предыдущем ответе. Когда выбирать: Продукт живет годами, команда растет, есть сложные кросс-фичевые сценарии (например, «Корзина» влияет на «Хедер», «Каталог», «Чек-аут»).
Уровень 3: Domain-Driven Design (DDD) + Modular Monolith (Сложные домены, фінтех, логистика) Фокус на Domain Layer (Entities, Value Objects, Aggregates, Domain Events, Repositories interfaces). Инфраструктура (БД, HTTP, Kafka) вынесена на периферию (Ports & Adapters / Hexagonal / Clean Architecture). Структура модуля (Bounded Context):
modules/
└── ordering/
├── domain/ # Чистый TS/Go, нет внешних зависимостей
│ ├── entity/ # Order (Aggregate Root), OrderItem
│ ├── value-object/ # Money, Address, OrderId
│ ├── event/ # OrderPlaced, OrderCancelled
│ ├── repository/ # Interface: OrderRepository
│ └── service/ # Domain Services (PricingService)
├── application/ # Use Cases / Commands / Queries (CQRS)
│ ├── command/ # PlaceOrderHandler
│ └── query/ # GetOrderDetailsHandler
├── infrastructure/ # Реализации интерфейсов
│ ├── persistence/ # TypeORM/Prisma/OrderRepositoryImpl
│ └── messaging/ # KafkaOrderEventPublisher
└── api/ # HTTP Controllers / gRPC / GraphQL Resolvers
Плюсы: Максимальная защита бизнес-логики, тестируемость, готовность к выделению в микросервис. Минусы: Высокий порог входа, overhead на простых CRUD, требует сильных Senior/Architect.
Уровень 4: Microkernel / Plugin Architecture (Платформы, Extensible Systems) Ядро (Kernel) + набор плагинов. Используется редко (IDE, CI/CD системы, платформы для маркетплейсов).
3. Процесс принятия решения на старте (Practical Checklist)
- Согласую с Tech Lead / Architect (если есть) и ведущими разработчиками. Архитектура — это социальный контракт.
- Определю «Architectural Quantum» (Квант архитектуры) — минимальную независимую единицу деплоя/разработки. Для монолита это модуль/слайс, для микросервисов — сервис.
- Зафиксирую ADR (Architecture Decision Record) в репозитории (
docs/adr/001-choose-frontend-architecture.md).- Контекст: Команда 4 фуллстек, продукт SaaS B2B, срок 1 год+.
- Решение: FSD на фронтенде, Modular Monolith (Go) на бэкенде.
- Последствия: Нужен линтер для FSD (eslint-plugin-fsd), код-ревью проверяет Public API, онбординг новых займет +1 неделя.
- Подготовлю скелет (Starter Kit) / Boilerplate с настроенными линтерами, тестами, CI/CD, чтобы команда не изобретала велосипед.
- План эволюции: Явно заложу, при каких условиях мы перейдем на следующий уровень (например: «Если команда > 8 человек или появится 2-й продукт на этом бэкенде — выделим Auth/Billing в отдельные микросервисы»).
4. Антипаттерны выбора (Red Flags)
- Resume Driven Development: Выбираем микросервисы/Клин Арчетиктуру/Event Sourcing, потому что «хочу в резюме», а не потому что нужно бизнесу.
- Over-engineering на старте: FSD/ДДД для лендинга или админки на 5 таблиц.
- Under-engineering для долгоживущего продукта: «Потом рефакторим» на проекте с горизонтом 3+ года и растущей командой. Технический долг растет экспоненциально.
- Игнорирование команды: Навязываем сложную архитектуру джуниорам без менторства → кодовая база превращается в хаос быстрее, чем без архитектуры.
- Копирование чужого опыта слепо: «В Google/Netflix так делают». У них другие масштабы, команда, культура, легаси.
5. Пример ответа на интервью (Senior/Tech Lead стиль)
> «Я не начинаю с выбора модного названия архитектуры. Сначала собираю контекст: срок жизни продукта, размер и состав команды, сложность домена, требования к времени выхода на рынок. > > Для MVP/внутренних инструментов выбираю Grouping by Feature с минимальными слоями (shared, features, app) — это дает скорость и простоту рефакторинга. > > Для продуктовых команд 5+ человек на горизонте 1+ год — стандарт де-факто FSD на фронте и Modular Monolith на бэке. Они дают лучший баланс изоляции, явных зависимостей и скорости онбординга. > > Если домен насыщен бизнес-логикой (финтех, сложные стейт-машины, инварианты) — ввожу DDD Tactical Patterns внутри модулей (Aggregates, Domain Events, Repository Pattern), изолируя ядро от инфраструктуры. > > Ключевой момент: любая архитектура требует enforcement (принуждения) — линтеры, архитектурные тесты (ArchUnit, dependency-cruiser), код-ревью чек-листы. Без этого это просто структура папок, которая разваливается за 3 месяца».
Резюме: Архитектура выбирается итеративно: стартуем проще (YAGNI), но с пониманием следующего шага эволюции, и жестко фиксируем правила игры через инструментарий (линтеры, тесты), а не только договоренности.
Вопрос 4. Как проектируете взаимодействие с API?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:02:55"/>
Ответ собеседника: правильный. Использует TanStack Query: отдельная папка api с эндпоинтами, обёртки-хуки поверх TanStack Query для асинхронных запросов.
Правильный ответ:
Взаимодействие с API — это не просто «вызовы fetch», а слой интеграции (Integration Layer), который изолирует приложение от деталей транспорта, схемы бэкенда и сетевых ошибок. Моя цель — сделать работу с данными типизированной, декларативной, кэшируемой и устойчивой к ошибкам.
1. Архитектурные слои (Separation of Concerns)
Делю слой доступа к данным на три уровня (в терминах FSD — это shared/api + entities/*/api + features/*/api):
| Слой | Ответственность | Пример расположения (FSD) |
|---|---|---|
| 1. Transport / HTTP Client | Низкоуровневый fetch/axios/ky: базовый URL, интерцепторы (auth, logging, retry), обработка сетевых ошибок, сериализация. | shared/api/http.ts, shared/api/instance.ts |
| 2. API Contracts / Endpoints | Типизированные функции для каждого эндпоинта. Зная только path, method, params, body, response. Не знают про кэш, UI, хуки. Чистые функции. | entities/user/api/userApi.ts, features/auth/api/authApi.ts |
| 3. Data Access / Hooks (Integration Layer) | Хуки (useQuery, useMutation), которые комбинируют эндпоинты с TanStack Query (кэш, инвалидация, оптимистичные обновления). Это Public API для UI-слоя (ui, model). | entities/user/model/hooks.ts, features/auth/model/hooks.ts |
> Принцип: UI-компоненты не знают про fetch, axios, URL, HTTP-коды, токены. Они вызывают useUserProfile() или useLoginMutation().
2. Генерация типов и контрактов (Contract-First Approach)
Ручное написание типов — источник багов и расхождений с бэкендом.
Стек: OpenAPI (Swagger) + Orval / openapi-typescript-codegen / Hey API (OpenAPI TS).
Workflow:
- Бэкенд публикует
openapi.json(в CI/CD или dev-портал). - В CI / pre-commit / локально скрипт генерирует:
- Zod / Valibot / TypeBox схемы (Runtime validation).
- TypeScript типы (Request/Response DTOs).
- Готовые клиентские функции (endpoints) под наш HTTP-клиент (TanStack Query, Axios, Fetch).
- Мы коммитим сгенерированный код (или генерируем в
node_modules/.cacheи импортируем через path alias).
Пример конфига Orval (orval.config.ts):
export default {
petstore: {
input: 'https://api.example.com/openapi.json',
output: {
target: 'shared/api/generated/endpoints.ts',
client: 'tanstack-query', // Генерирует хуки сразу!
override: {
mutator: {
path: 'shared/api/http.ts',
name: 'customFetcher',
},
query: {
useQuery: true,
useMutation: true,
},
},
schemas: 'zod', // Генерируем Zod схемы для валидации рантайма
},
},
};
Результат: При изменении контракта бэкенда — TypeScript ошибка в CI на фронте до деплоя. Никаких any, никаких console.error("unexpected field") в проде.
3. HTTP Client Setup (Shared Layer)
Использую Ky (обертка над fetch) или Axios с кастомным инстансом. Ключевые интерцепторы:
// shared/api/http.ts
import ky from 'ky';
import { getAuthToken, refreshAuthToken, logout } from 'entities/auth';
export const api = ky.create({
prefixUrl: import.meta.env.VITE_API_URL,
credentials: 'include', // для HttpOnly cookies
hooks: {
beforeRequest: [
(request) => {
const token = getAuthToken();
if (token) request.headers.set('Authorization', `Bearer ${token}`);
},
],
afterResponse: [
async (request, options, response) => {
if (response.status === 401) {
// Пытаемся рефрешнуть токен 1 раз
try {
await refreshAuthToken();
// Повторяем оригинальный запрос с новым токеном
return ky(request);
} catch {
logout(); // Редирект на логин
throw response; // Прерываем цепочку
}
}
// Парсим JSON только если есть тело, иначе возвращаем response
if (response.status !== 204) return response.json();
return response;
},
],
},
// Глобальная обработка ошибок сети/парсинга
retry: { limit: 2, methods: ['get', 'head', 'put', 'delete'] }, // Идемпотентные
});
Валидация ответа в рантайме (Zod):
// entities/user/api/userApi.ts
import { api } from 'shared/api/http';
import { UserSchema, type User } from './schemas'; // Сгенерировано Orval'ом
export const getUserProfile = async (): Promise<User> => {
const response = await api.get('user/profile').json(); // Ky парсит JSON
// Защита "на входе" в приложение
return UserSchema.parse(response);
};
4. Интеграция с TanStack Query (React Query) — Слой Хуков
Не пишу useQuery в компонентах. Создаю Domain Hooks (в model/hooks.ts или api/hooks.ts).
Принципы:
- Query Keys Factory — централизованная фабрика ключей для инвалидации.
- Типизация хуков —
UseQueryResult<User, ApiError>. - Инкапсуляция бизнес-логики кэша (инвалидация, оптимистичные обновления) внутри хука.
// entities/user/model/hooks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userApi } from '../api/userApi';
import { userKeys } from './queryKeys';
// Фабрика ключей — единственный источник правды
export const userKeys = {
all: ['user'] as const,
profile: () => [...userKeys.all, 'profile'] as const,
list: (filters: UserFilters) => [...userKeys.all, 'list', filters] as const,
};
// --- SELECTORS (Чтение) ---
export const useUserProfile = (options?: UseQueryOptions<User, ApiError>) => {
return useQuery({
queryKey: userKeys.profile(),
queryFn: userApi.getProfile,
staleTime: 1000 * 60 * 5, // 5 мин свежие данные
...options,
});
};
export const useUserList = (filters: UserFilters) => {
return useQuery({
queryKey: userKeys.list(filters),
queryFn: () => userApi.getList(filters),
placeholderData: (prev) => prev, // keepPreviousData (deprecated v5)
});
};
// --- MUTATIONS (Запись) ---
export const useUpdateProfile = () => {
const qc = useQueryClient();
return useMutation({
mutationFn: userApi.updateProfile,
// Оптимистичное обновление
onMutate: async (newData) => {
await qc.cancelQueries({ queryKey: userKeys.profile() });
const previous = qc.getQueryData<User>(userKeys.profile());
qc.setQueryData<User>(userKeys.profile(), (old) => ({ ...old, ...newData }));
return { previous };
},
onError: (err, vars, context) => {
if (context?.previous) qc.setQueryData(userKeys.profile(), context.previous);
// Тут можно показать тост через toast.error(err.message)
},
onSettled: () => {
qc.invalidateQueries({ queryKey: userKeys.profile() }); // Рефетч после мутации
},
});
};
5. Обработка ошибок (Error Handling Strategy)
Разделяю ошибки на уровни:
| Тип ошибки | Где обрабатываем | UI Реакция |
|---|---|---|
| Network / 5xx / Timeout | http.ts (interceptor) -> ApiError | Глобальный Toast / Offline Banner / Retry кнопка |
| 401 Unauthorized | http.ts (Auto Refresh) -> если не вышло -> AuthModel.logout() | Редирект на /login с returnUrl |
| 403 Forbidden | http.ts или useQuery onError | Toast "Нет прав", скрытие UI элементов (через RBAC store) |
| 400 / 422 Validation | Mutation Hook (onError) -> error.response.data (Zod/Valibot) | Полевая валидация формы (react-hook-form setError) |
| 404 Not Found | Query Hook (useQuery throwOnError: false) | Рендер NotFound страницы или пустого состояния в UI |
Business Logic Errors (Код 200, но { success: false, code: 'INSUFFICIENT_FUNDS' }) | API Endpoint -> парсинг в ApiError с code | Mutation Hook -> onError -> Toast / Modal / Redirect |
Типизация ошибки (ApiError):
// shared/api/types.ts
export class ApiError extends Error {
constructor(
public readonly status: number,
public readonly code: string, // Бэкендовый код ошибки
public readonly message: string,
public readonly details?: Record<string, string[]> // Полевая валидация 422
) {
super(message);
this.name = 'ApiError';
}
static fromResponse(response: Response, data: unknown): ApiError {
// Парсим стандартный формат ошибки бэкенда { code, message, details }
}
}
6. Работа с формами и мутациями (React Hook Form + Zod + TanStack Query)
Стандартный паттерн для форм:
// features/profile/ui/ProfileForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useUpdateProfile } from 'entities/user/model/hooks';
import { updateProfileSchema, type UpdateProfileDto } from 'entities/user/api/schemas';
export const ProfileForm = () => {
const { register, handleSubmit, setError, formState: { isSubmitting } } = useForm<UpdateProfileDto>({
resolver: zodResolver(updateProfileSchema),
defaultValues: { name: '', email: '' }, // или из queryData
});
const { mutate: updateProfile, isPending } = useUpdateProfile();
const onSubmit = (data: UpdateProfileDto) => {
updateProfile(data, {
onSuccess: () => toast.success('Профиль обновлен'),
onError: (error) => {
if (error.details) {
// Распределяем ошибки полей от бэкенда
Object.entries(error.details).forEach(([field, messages]) => {
setError(field as keyof UpdateProfileDto, { message: messages[0] });
});
} else {
toast.error(error.message);
}
},
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} disabled={isPending} />
<button type="submit" disabled={isPending}>Сохранить</button>
</form>
);
};
7. Чек-лист Code Review для API-слоя (Senior Checklist)
- Нет
fetch/axiosв компонентах? Только хукиuse.... - Есть генерация типов из OpenAPI? Нет ручных
interface UserDto. - Есть Runtime Validation (Zod/Valibot) на входящих данных?
parseв эндпоинте. - Query Keys вынесены в фабрику? Нет строковых литералов
['user', id]разбросанных по коду. - Настроен
staleTime/gcTime(cacheTime)? Нет лишних рефетчей при фокусе окна. - Мутации инвалидируют правильные ключи?
invalidateQueries({ queryKey: userKeys.all })или точечно. - Оптимистичные обновления имеют
onErrorrollback? - Обработка 401/403 централизована в HTTP-клиенте? Нет проверок в каждом хуке.
- Единый формат ошибки (
ApiError)? Бэкенд присылает единый формат, фронт парсит его в класс. - Нет утечек памяти?
AbortControllerработает (React Query делает авто, но кастомныеfetch— нет).
Резюме (TL;DR для ответа на собеседовании)
> «Строю Contract-First интеграцию: OpenAPI → Orval/HeyAPI → Типы + Zod-схемы + Эндпоинты.
>
> Архитектура из 3 слоев: HTTP Client (Ky/Axios + Interceptors) → Typed Endpoints (Pure Functions + Validation) → Domain Hooks (TanStack Query Factory + Optimistic Updates + Error Mapping).
>
> UI знает только хуки (useUserProfile, useLoginMutation). Все сетевые ошибки, рефреши токенов, валидация, кэширование инкапсулированы внутри. Настроен строгий staleTime, фабрика queryKeys, централизованная обработка 401/422/500. Никаких any, никаких ручных типов для DTO».
Вопрос 5. Как обрабатываете нестабильное соединение и частичную загрузку данных?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:03:30"/>
Ответ собеседника: неполный. Опирается на встроенные механизмы TanStack Query: ретраи, кэширование, поллинг для актуальных данных; кастомной обработки качества связи (offline detection, адаптивные ретраи) не реализовывал.
Правильный ответ (Senior / Tech Lead уровень)
Встроенные механизмы TanStack Query (retry, cacheTime, refetchOnWindowFocus) — это база (фундамент), но для продакшн-приложений на нестабильной связи (мобильная сеть, метро, слабый Wi-Fi) этого недостаточно. Нужен комплексный подход: детекция качества связи → адаптивные стратегии → персистентность → UX для частичных данных → офлайн-очередь мутаций.
1. Детекция состояния сети и качества связи (Network Quality Detection)
Нельзя полагаться только на navigator.onLine (он показывает только наличие линка до роутера, не до интернета).
Стратегия: Active Probing + Browser Signals
// shared/lib/network/quality.ts
import { NetInfo, NetInfoState } from '@react-native-community/netinfo'; // RN
// Для Web: navigator.connection (Network Information API) + кастомный пинг
type NetworkQuality = 'offline' | 'poor' | 'slow' | 'fast' | 'unknown';
interface NetworkState {
quality: NetworkQuality;
isOnline: boolean;
downlink?: number; // Mbps
rtt?: number; // Round Trip Time ms
effectiveType?: '2g' | '3g' | '4g' | 'slow-2g';
}
const listeners = new Set<(state: NetworkState) => void>();
let currentState: NetworkState = { quality: 'unknown', isOnline: true };
// 1. Passive: Network Information API (Chrome, Edge, Firefox)
if (typeof navigator !== 'undefined' && 'connection' in navigator) {
const conn = (navigator as any).connection;
const update = () => {
currentState = {
...currentState,
isOnline: navigator.onLine,
downlink: conn.downlink,
rtt: conn.rtt,
effectiveType: conn.effectiveType,
quality: mapEffectiveTypeToQuality(conn.effectiveType, conn.downlink, conn.rtt),
};
notify();
};
conn.addEventListener('change', update);
update();
}
// 2. Active: Background Probe (Health Check) - реальная проверка доступа к API
let probeInterval: ReturnType<typeof setInterval>;
const PROBE_URL = `${import.meta.env.VITE_API_URL}/health`; // Легкий эндпоинт 204 No Content
const PROBE_INTERVAL_FAST = 10_000; // 10 сек когда плохо
const PROBE_INTERVAL_SLOW = 60_000; // 1 мин когда хорошо
const probe = async () => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
try {
const start = performance.now();
await fetch(PROBE_URL, { method: 'HEAD', cache: 'no-store', signal: controller.signal });
const latency = performance.now() - start;
currentState = { ...currentState, isOnline: true, quality: latency < 500 ? 'fast' : latency < 1500 ? 'slow' : 'poor' };
} catch {
currentState = { ...currentState, isOnline: false, quality: 'offline' };
} finally {
clearTimeout(timeout);
notify();
// Адаптивный интервал
clearInterval(probeInterval);
probeInterval = setInterval(probe, currentState.quality === 'fast' ? PROBE_INTERVAL_SLOW : PROBE_INTERVAL_FAST);
}
};
function mapEffectiveTypeToQuality(type: string, downlink: number, rtt: number): NetworkQuality {
if (!navigator.onLine) return 'offline';
if (type === 'slow-2g' || type === '2g' || downlink < 0.5 || rtt > 2000) return 'poor';
if (type === '3g' || downlink < 1.5 || rtt > 800) return 'slow';
return 'fast';
}
export const networkQuality = {
getState: () => currentState,
subscribe: (fn: (s: NetworkState) => void) => {
listeners.add(fn);
fn(currentState);
return () => listeners.delete(fn);
},
startProbing: () => probe(),
};
Интеграция в React (Context + Hook):
// shared/lib/network/NetworkProvider.tsx
export const NetworkProvider = ({ children }) => {
const [state, setState] = useState(networkQuality.getState());
useEffect(() => networkQuality.subscribe(setState), []);
useEffect(() => networkQuality.startProbing(), []);
return <NetworkContext.Provider value={state}>{children}</NetworkContext.Provider>;
};
export const useNetworkQuality = () => useContext(NetworkContext);
2. Адаптивная конфигурация TanStack Query (Adaptive Query Config)
Используем queryClient.setDefaultOptions или кастомный хук useAdaptiveQuery, который читает useNetworkQuality().
// shared/api/adaptiveQuery.ts
import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { useNetworkQuality } from 'shared/lib/network';
export const useAdaptiveQuery = <TData, TError>(
key: QueryKey,
fn: () => Promise<TData>,
options?: Partial<UseQueryOptions<TData, TError>>
) => {
const { quality, isOnline } = useNetworkQuality();
// Базовая конфигурация
const baseOptions: UseQueryOptions<TData, TError> = {
queryKey: key,
queryFn: fn,
// 1. Кэш — король офлайна
gcTime: 1000 * 60 * 60 * 24, // 24 часа (было cacheTime)
staleTime: 1000 * 60 * 5, // 5 мин свежести
// 2. Ретраи: адаптивные
retry: quality === 'offline' ? 0 : quality === 'poor' ? 5 : 3,
retryDelay: (attemptIndex) => {
// Экспоненциальный бэкофф + джиттер
const base = Math.min(1000 * 2 ** attemptIndex, 30_000);
const jitter = Math.random() * 1000;
return quality === 'poor' ? base * 2 + jitter : base + jitter; // Дольше ждем на плохой связи
},
// 3. Рефетчинг: отключаем агрессивный на плохой связи
refetchOnWindowFocus: quality !== 'poor' && quality !== 'offline',
refetchOnReconnect: isOnline && quality !== 'poor', // Не спамить сразу после реконнекта на 2G
refetchInterval: quality === 'fast' ? false : 1000 * 60 * 2, // Поллинг только если не fast (для крит. данных)
// 4. Плейсхолдеры для частичной загрузки (см. п.3)
placeholderData: (prev) => prev, // keepPreviousData (v4) -> placeholderData (v5)
...options,
};
// Если офлайн — не запускаем запрос, возвращаем кэш
const enabled = options?.enabled !== false && isOnline;
return useQuery({ ...baseOptions, enabled });
};
3. Работа с частичными данными (Partial Data & Streaming UX)
Проблема: большой список/дашборд грузится 10 сек. Пользователь видит спиннер.
Решения:
А. select + placeholderData (Structural Sharing)
Компонент подписывается только на нужный кусок. Пока грузится новый — показывает старый (частичный).
// Хук возвращает только ids для списка (быстро) + флаг isLoading
const useUserIds = () => useAdaptiveQuery(['users', 'list'], fetchUsers, {
select: (data) => data.map(u => u.id), // Селектор на уровне подписки
placeholderData: (prev) => prev, // Показывает старые ID мгновенно
});
// Компонент списка
const UserList = () => {
const { data: ids, isFetching } = useUserIds(); // isFetching=true -> фоновый рефетч
const { data: users } = useUsersByIds(ids); // Отдельный запрос деталей (или предзагружены)
return (
<div className={isFetching ? 'opacity-50' : ''}>
{ids?.map(id => <UserCard key={id} id={id} />)}
</div>
);
};
Б. Suspense + Streaming (React 18 + RSC / TanStack Query v5 suspense: true)
Для SSR/SSG или клиентского Suspense:
const UserProfile = ({ userId }) => {
// Бросает Promise, Suspense Boundary показывает Skeleton
const { data: user } = useAdaptiveQuery(['user', userId], () => fetchUser(userId), {
suspense: true,
// useErrorBoundary: true
});
return <ProfileCard user={user} />;
};
// Родитель
<Suspense fallback={<ProfileSkeleton />}>
<UserProfile userId={123} />
</Suspense>
В. Infinite Query / Pagination (Виртуализация)
Для бесконечных лент — useInfiniteQuery + react-window / @tanstack/react-virtual. Грузим порциями, рендерим только видимое.
Г. Server-Sent Events (SSE) / WebSockets для "хвоста"
Если данные обновляются часто — не поллим. Подключаем SSE для инвалидации/пуша.
// shared/api/realtime.ts
export const subscribeToUpdates = (onUpdate: (data: UpdateEvent) => void) => {
const es = new EventSource('/api/stream/updates');
es.onmessage = (e) => onUpdate(JSON.parse(e.data));
return () => es.close();
};
// В хуке сущности
useEffect(() => {
if (networkQuality.getState().quality !== 'offline') {
return subscribeToUpdates((update) => {
qc.setQueryData(['entity', update.id], (old) => mergeDeep(old, update));
});
}
}, []);
4. Офлайн-персистентность и Очередь мутаций (Offline First / Mutation Queue)
Ключ: persistQueryClient (IndexedDB) + mutationCache + persistMutation (или кастомная очередь).
// shared/api/queryClient.ts
import { QueryClient, PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { IndexedDBPersister } from '@tanstack/query-indexed-db-persister'; // Лучше для объемов
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: 3 } },
mutationCache: new MutationCache({
onError: (error, variables, context, mutation) => {
// Логируем в Sentry/LogRocket
},
onSuccess: (data, variables, context, mutation) => {
// Инвалидация кэшей после успешной мутации (даже если была офлайн)
},
}),
});
// Персистер для КЕША (GET запросы)
const persister = createSyncStoragePersister({
storage: window.localStorage, // или IndexedDBPersister
throttleTime: 1000,
// Сериализуем только успешные данные, не ошибки
serialize: (client) => JSON.stringify(client.getQueryCache().getAll().map(q => q.state)),
});
// Персистентность МУТАЦИЙ (Offline Queue) - требует кастомной реализации или библиотеки
// @tanstack/query-offline-mutations (эксперименталь) или своя обертка
Паттерн "Optimistic Update + Offline Queue":
// features/comments/api/useAddComment.ts
export const useAddComment = () => {
const qc = useQueryClient();
const { isOnline } = useNetworkQuality();
return useMutation({
mutationFn: (comment) => api.comments.create(comment),
// 1. Оптимистичное обновление UI мгновенно
onMutate: async (newComment) => {
await qc.cancelQueries({ queryKey: ['comments', newComment.postId] });
const prev = qc.getQueryData(['comments', newComment.postId]);
const tempId = `temp-${Date.now()}`;
qc.setQueryData(['comments', newComment.postId], (old) => [
...(old || []),
{ ...newComment, id: tempId, pending: true, local: true } // Флаг для UI
]);
return { prev };
},
// 2. Если онлайн и успешно — заменяем tempId на реальный
onSuccess: (serverComment, variables, context) => {
qc.setQueryData(['comments', variables.postId], (old) =>
old.map(c => c.id === `temp-${variables.clientId}` ? serverComment : c)
);
},
// 3. Если ошибка сети (офлайн) — оставляем в кэше с флагом pending=true
// TanStack Query v5 сохраняет мутацию в mutationCache автоматически при retry.
// Нужно настроить persistence для mutationCache.
onError: (err, vars, context) => {
if (context?.prev) qc.setQueryData(['comments', vars.postId], context.prev);
// Показываем тост "Отправлено, когда появится сеть"
},
// 4. Ретраи только онлайн
retry: (failureCount, error) => isOnline && failureCount < 3,
});
};
UI для состояний мутации:
const CommentItem = ({ comment }) => {
const { isPending, isPaused } = useMutationState({ // или переменные из мутации
filters: { mutationKey: ['addComment'] },
select: (m) => m.variables.clientId === comment.id
});
if (comment.local && !comment.id?.startsWith('temp')) return null; // Уже закоммитился
return (
<div className={comment.pending ? 'opacity-50' : ''}>
{comment.text}
{comment.pending && <span className="badge">⏳ В очереди...</span>}
{isPaused && <span className="badge warning">📴 Офлайн, ждет сети</span>}
</div>
);
};
5. Conflict Resolution (Разрешение конфликтов при синхронизации)
Когда пользователь был офлайн, отредактировал профиль, а на сервере его изменил админ.
Стратегии:
- Last Write Wins (LWW) — просто перезаписываем (просто, теряем данные).
- Server Wins — откатываем локальные изменения, показываем уведомление "Данные обновлены сервером".
- Merge (Operational Transform / CRDT / Manual) — сложно, для коллаборации (Figma, Google Docs).
- User Choice (Рекомендуемый для форм) — при
onSuccessсинхронизации офлайн-мутации проверяемETag/version. Если конфликт — открываем модалку "Ваши изменения конфликтуют с сервером. Что оставить?".
// В onSuccess синхронизации
onSuccess: (serverData, variables, context) => {
if (serverData.version !== variables.baseVersion) {
// Конфликт!
openConflictModal({
local: variables.data,
server: serverData,
onResolve: (merged) => api.profile.update(merged) // Ресабмит
});
}
}
6. Чек-лист готовности к нестабильной связи (Production Readiness)
| Категория | Проверка | Инструмент / Код |
|---|---|---|
| Кэш | gcTime (v5) / cacheTime (v4) >= 24h для GET? | QueryClient config |
| Кэш | staleTime настроен разумно (не 0)? | QueryClient / useQuery |
| Ретраи | Экспоненциальный бэкофф + джиттер? | retryDelay function |
| Ретраи | Адаптивны под качество связи (effectiveType)? | useNetworkQuality hook |
| Офлайн | Данные видны при navigator.onLine === false? | persistQueryClient (IndexedDB) |
| Мутации | Офлайн-очередь мутаций работает? | persistMutationCache / Custom Queue |
| Мутации | Оптимистичный UI + Rollback на ошибку? | onMutate / onError / onSettled |
| UI | Скелетоны / Плейсхолдеры вместо спинеров? | placeholderData, Suspense |
| UI | Индикатор "Вы офлайн" / "Синхронизация..."? | useNetworkQuality Context |
| Конфликты | Есть стратегия для одновременных правок? | Versioning / ETags / Conflict Modal |
| Тестирование | Е2Е тесты в Throttling (Slow 3G / Offline)? | Chrome DevTools / Cypress cy.throttle |
Резюме (TL;DR для ответа)
> «База — TanStack Query дефолты (retry, cache, refetchOnReconnect). Но для продакшена на мобильной связи это мало.
>
> Добавляю 4 слоя:
> 1. Network Quality Detection (navigator.connection + Active Probe /health) → React Context.
> 2. Adaptive Query Config — хуки читают качество связи: на 2G/Offline отключаю refetchOnFocus, увеличиваю retryDelay, ставлю retry: 5, включаю поллинг только для критики.
> 3. Partial Data UX — placeholderData (keep previous), select для подписки на куски, Suspense + Skeletons, Infinite Query + Virtualization.
> 4. Offline Mutations — persistQueryClient (IndexedDB) для кэша GET + кастомная персистентная очередь мутаций (Optimistic Update + pending флаг + ретраи при реконнекте + Conflict Resolution по версиям/ETag).
>
> Все это обернуто в кастомные хуки (useAdaptiveQuery, useOfflineMutation), чтобы компоненты оставались чистыми».
Вопрос 7. Как выбираете между Next.js и чистым Reactом для проекта?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:05:25"/>
Ответ собеседника: правильный. Next.js — для публичных проектов с требованиями к SEO, SSR/SSG, первичной загрузке; чистый React (CSR) — для закрытых админок и внутренних инструментов. App Router упрощает настройку рендеринга.
Правильный ответ:
Выбор между Next.js (App Router) и чистым React (Vite/Rspack + React Router / TanStack Router) — это архитектурное решение, влияющее на инфраструктуру, стоимость хостинга, DX и возможности продукта. На уровне Tech Lead мы оцениваем не просто «нужен SEO или нет», а набор компромиссов.
1. Чек-лист принятия решения (Decision Matrix)
| Критерий | Next.js (App Router) | Чистый React (CSR/SPA) |
|---|---|---|
| SEO / Social Previews | Нативно (SSR, SSG, ISR, generateMetadata). Критично для лендингов, блогов, маркетплейсов, публичных каталогов. | Требует отдельного решения: Prerender.io, Puppeteer-сервис, или отказ от SEO. |
| Первичная загрузка (FCP/LCP) | SSR/Streaming — сервер шлет HTML сразу. Лучше для медленных сетей/устройств. | Пустой index.html -> загрузка JS -> рендер. Требует Skeleton/Loader. Хуже LCP на слабых клиентах. |
| Тип приложения | Публичные, контентные, e-commerce, маркетинговые, документация. | Приватные: Админки, Дашборды, SaaS-кабинеты, B2B-панели, внутренние инструменты, редакторы (Figma-like). |
| Авторизация / Приватность | Сложнее: Middleware, Server Actions, защита layouts, noStore. Риск утечки данных в SSR-кэше. | Проще: Вся логика на клиенте. Защита роутов — просто обертка <PrivateRoute>. Нет риска SSR-утечек. |
| Инфраструктура / Хостинг | Node.js Runtime обязателен (Vercel, Netlify, Docker на K8s/VM, AWS Lambda@Edge). Cold starts на Serverless. Edge Runtime — ограничения API. | Статические файлы (Nginx, S3+CloudFront, Cloudflare Pages, GitHub Pages). Дешево, быстро, масштабируется бесплатно. Нет Node.js на проде. |
| Backend Coupling | BFF паттерн нативно (Server Actions, Route Handlers, прямые вызовы БД/ORM). Можно не писать отдельный API. | Чистое разделение: Отдельный API (Go/Java/Node/Python). Фронт — просто статический клиент. Лучше для микросервисной архитектуры. |
| Bundle Size / JS на клиенте | Больше (React Server Components + Client Components гидрация). next/script для управления. | Контролируешь полностью. Можно сделать очень легким (Preact, ручная оптимизация). |
| Real-time / WebSockets | Сложнее: нужен отдельный сервер (Socket.io, Pusher, Centrifugo) или Edge WebSockets. Next-сервер не держит долгое соединение. | Так же нужен отдельный WS-сервер. Но нет иллюзии, что «Next сам справится». |
| Команда / DX | Высокий порог входа (RSC, Suspense, Server Actions, Cache Tagging). Требует знания веб-основ (HTTP, Streaming). | Ниже порог входа. Стандартный React + Router. Проще онбординг джунов. |
| Кэширование | Мощная, но сложная система (fetch cache, revalidateTag, noStore, unstable_cache). Легко выстрелить в ногу (показать старые данные). | Только клиентский кэш (TanStack Query, SWR). Предсказуемо. Серверный кэш — задача бэкенда. |
2. Когда однозначно Next.js (App Router)
- Публичный контент с SEO: Интернет-магазины (PDP, PLP), блоги, новостные порталы, лендинги, каталоги компаний.
- Нужен BFF (Backend for Frontend): Агрегация данных из нескольких микросервисов, скрытие схем БД, авторизация через куки на Edge/Node. Server Actions идеально подходят для форм (мутации).
- Гибридный рендеринг: Часть страниц статична (SSG/ISR), часть динамична (SSR), часть интерактивна (Client Components). App Router решает это через Layouts и Segment Config.
- Streaming SSR + Suspense: Критично для сложных дашбордов с разной скоростью загрузки виджетов (показывает каркас, подгружает тяжелые части по мере готовности).
- Internationalization (i18n): Встроенная маршрутизация (
[locale]/...), генерация метаданных,accept-languageредиректы в Middleware.
3. Когда однозначно Чистый React (Vite + React Router v7 / TanStack Router)
- Админки / B2B-панели / Внутренние инструменты: За логином, SEO не нужен, авторизация сложная (RBAC, ABAC), много состояния на клиенте (Redux/Zustand/Jotai, WebSockets, Canvas/WebGL редакторы).
- Статический хостинг / Cost Saving: Хранить на S3 + CloudFront за копейки. Нет денег/компетенций на поддержку Node.js кластера / Vercel Enterprise.
- Отдельный Backend Team / Contract First: Бэкенд выдает OpenAPI/GraphQL. Фронт — просто потребитель. Не нужно смешивать логику в Server Actions.
- Heavy Client State / Offline-first: Локальные БД (RxDB, WatermelonDB, IndexedDB), сложные оптимистичные обновления, CRDT. Next.js RSC здесь мешает (сериализация, границы Client/Server).
- Миграция легаси: Постепенный перевод старого CRA/SPA на Vite + React Router v7 (поддерживает SSR опционально, если вдруг понадобится).
4. Нюансы App Router, о которых молчат на докладах (Senior View)
- Server Components по умолчанию — это смена парадигмы.
useState,useEffect, браузерные API (window,localStorage) не работают в корне компонентов. Приходится оборачивать в'use client'провайдеры (Theme, Auth, Tooltips, WebSocket). Это фрагментирует код. - Кэширование
fetchпо умолчаниюforce-cache. Самая частая бага: «Обновил данные в БД, на сайте старые». Лечитсяexport const revalidate = 0/noStore()/revalidateTag(), но требует дисциплины. - Server Actions — не панацея. Это RPC-вызовы. Сложно типизировать ошибки (возвращать
Result<T, E>вместоthrow), сложно делать редиректы (redirect()бросает исключение), нет нативной валидации (нужен Zod внутри). - Bundle Size: RSC уменьшает JS на клиенте, но увеличивает HTML (RSC Payload). На медленных сетях это может быть хуже, чем сжатый gzip/brotli JS-бандл, если HTML огромный.
- Middleware Edge Runtime: Ограниченный API (нет
fs, нет нативных модулей, лимит CPU/памяти). Подходит только для редиректов, рерайтов, установки кук, легкой авторизации.
5. Гибридный подход (Monorepo)
В крупных продуктах часто лучше разделить:
apps/web(Next.js) — Публичная часть: Лендинг, Блог, Каталог, Чекаут (SEO, SSR).apps/dashboard(Vite + React Router / TanStack Router) — Приватный кабинет: Настройки, Аналитика, Редакторы (CSR, сложный стейт, WS).packages/ui— Общий UI Kit (Storybook).packages/api-client— Типизированный клиент (Orval / openapi-fetch / GraphQL Codegen).
Это дает лучшее UX для маркетинга и лучший DX/Performance для продукта.
Резюме (TL;DR для ответа)
> «Выбираю по типу продукта и инфраструктурным ограничениям. > > Next.js (App Router) — дефолт для публичных проектов, где критичны SEO, LCP, Social Previews, и где удобен BFF-паттерн (Server Actions, агрегация данных). Готов платить за Node.js хостинг и сложность кэширования. > > Чистый React (Vite + TanStack Router/React Router v7) — для приватных админок, дашбордов, сложных клиентских приложений (редакторы, реал-тайм, офлайн). Статический хостинг, предсказуемый кэш, простое разделение FE/BE, меньше JS на клиенте если не нужен RSC. > > В энтерпрайзе часто разделяю в монорепо: Next.js для маркетинга/чекаута, SPA для кабинета пользователя».
Вопрос 8. Какие инструменты управления состоянием используете и почему?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:06:31"/>
Ответ собеседника: правильный. Zustand для быстрых микро-сторов и простых кейсов; Redux Toolkit + RTK Query — для сложных приложений с глобальным стором. Серверное состояние разделяет: TanStack Query или RTK Query.
Правильный ответ:
Выбор инструмента управления состоянием диктуется классификацией состояния (Server vs Client, Global vs Local, Sync vs Async) и масштабом/сложностью приложения. Не существует «лучшего» инструмента, есть подходящий под слой архитектуры.
1. Классификация состояния (Ментальная модель)
Перед выбором библиотеки делю состояние на 4 квадранта:
| Тип состояния | Примеры | Инструмент | Почему |
|---|---|---|---|
| Server State (Асинхронное, кэшируемое, общее) | Списки товаров, профиль юзера, корзина (с бэкенда), настройки из БД | TanStack Query (React Query) / RTK Query | Автоматическое кэширование, дедупликация запросов, инвалидация, рефетчинг, оптимистичные обновления, пагинация, префетчинг. Redux/Zustand для этого — антипаттерн. |
| Global Client State (Синхронное, редко меняется, нужно везде) | Тема (dark/light), локаль, авторизация (токен/роли), сайдбар открыт/закрыт, уведомления (toasts) | Zustand / Jotai / Context + useReducer | Простой API, нет провайдеров (Zustand), атомарность (Jotai), селекторы для предотвращения ре-рендеров. |
| Local / UI State (Эфемерное, привязано к компоненту) | Состояние модалки, инпута, табов, анимации, драг-н-дроп | useState / useReducer / useRef | Колокация (colocation) — состояние живет там, где используется. Не загрязняет глобальный стор. |
| Complex Client State (Синхронное, сложные связи, много мутаций, история) | Редактор диаграмм, корзина со сложной логикой промо/скидок на клиенте, офлайн-очередь, undo/redo | Redux Toolkit (RTK) / XState (State Machines) | RTK: DevTools (time-travel), строгая структура (slice), middleware, сериализуемость, нормализация (createEntityAdapter). XState: визуализация логики, невозможность невалидных состояний. |
2. Мой стек по умолчанию (Default Stack) и обоснование
А. Server State: TanStack Query (React Query) v5
Стандарт де-факто для работы с асинхронностью.
Почему не RTK Query?
- RTK Query привязывает вас к Redux-экосистеме и его boilerplate (store, provider, типизация hooks).
- TanStack Query framework-agnostic (работает на Solid, Vue, Svelte), легче, гибче в настройке кэша (
gcTime,staleTime,structuralSharing), имеет лучшие DevTools (React Query Devtools) и паттерны:useSuspenseQuery,useQueryClient().setQueryData(оптимистичные апдейты),queryClient.cancelQueries. - Persist Query Client (IndexedDB) для офлайн-кэша работает из коробки.
Ключевые практики:
// shared/api/queryClient.ts — Синглтон с настройками по умолчанию
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 мин данные "свежие" -> нет лишних рефетчей
gcTime: 1000 * 60 * 60 * 24, // 24 часа в кэше (IndexedDB persist)
retry: (failureCount, error) => {
if (error instanceof ApiError && error.status === 401) return false; // Не ретраим 401
return failureCount < 3;
},
refetchOnWindowFocus: 'always', // или false для админок
structuralSharing: true, // Не триггерит ререндер, если данные === по ссылке
},
},
});
// features/user/hooks/useUser.ts — Инкапсуляция
export const userKeys = {
all: ['users'] as const,
detail: (id: string) => [...userKeys.all, 'detail', id] as const,
};
export const useUser = (id: string, options?: UseQueryOptions<User>) => {
return useQuery({
queryKey: userKeys.detail(id),
queryFn: () => api.users.get(id),
enabled: !!id,
...options,
});
};
// Оптимистичное обновление (Mutation)
export const useUpdateUser = () => {
const qc = useQueryClient();
return useMutation({
mutationFn: api.users.update,
onMutate: async (newData) => {
await qc.cancelQueries({ queryKey: userKeys.detail(newData.id) });
const prev = qc.getQueryData<User>(userKeys.detail(newData.id));
qc.setQueryData(userKeys.detail(newData.id), (old) => ({ ...old, ...newData }));
return { prev };
},
onError: (err, vars, ctx) => qc.setQueryData(userKeys.detail(vars.id), ctx?.prev),
onSettled: (data, err, vars) => qc.invalidateQueries({ queryKey: userKeys.detail(vars.id) }),
});
};
Б. Global Client State: Zustand (v4+)
Для UI-состояния, темы, авторизации, модалок — там, где не нужен Time-Travel.
Почему не Context API?
Contextвызывает ре-рендер всех консьюмеров при любом изменении значения. Нет встроенных селекторов (нужноuseContextSelectorили мемоизировать объекты вручную).- Zustand — это хук, не провайдер. Подписка на изменение конкретного поля (
useStore(state => state.theme)) не триггерит рендер при сменеstate.sidebarOpen.
Почему не Jotai/Recoil?
- Атомарный подход отличен для вычисляемых данных и сложных графов зависимостей, но избыточен для простых флагов/тем. Zustand проще в онбординге.
Паттерн "Store per Feature" (FSD style):
// entities/user/model/store.ts
import { createStore } from 'zustand/vanilla'; // vanilla для SSR/Next.js без провайдера
import { persist, createJSONStorage } from 'zustand/middleware';
type UserState = {
theme: 'light' | 'dark';
setTheme: (t: 'light' | 'dark') => void;
sidebarOpen: boolean;
toggleSidebar: () => void;
// Auth
token: string | null;
setToken: (t: string | null) => void;
// Hydration flag для Next.js (избегаем mismatch)
_hasHydrated: boolean;
setHasHydrated: (v: boolean) => void;
};
export const useUserStore = createStore<UserState>()(
persist(
(set) => ({
theme: 'light',
setTheme: (t) => set({ theme: t }),
sidebarOpen: true,
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
token: null,
setToken: (t) => set({ token: t }),
_hasHydrated: false,
setHasHydrated: (v) => set({ _hasHydrated: v }),
}),
{
name: 'app-user-storage',
storage: createJSONStorage(() => localStorage), // или sessionStorage
onRehydrateStorage: () => (state) => state?.setHasHydrated(true),
// Частичная гидратация: не сохраняем sidebarOpen
partialize: (state) => ({ theme: state.theme, token: state.token }),
}
)
);
// Хуки-селекторы для удобства (в features/entities)
export const useTheme = () => useUserStore((s) => s.theme);
export const useSetTheme = () => useUserStore((s) => s.setTheme);
В. Complex Client State / Forms: Redux Toolkit (RTK) + createEntityAdapter
Когда нужен: Нормализованное хранилище (Entity Adapter), сложные связанные мутации, Undo/Redo, Middleware (логирование, синхронизация с WS), Time-Travel Debugging.
Кейс: Корзина интернет-магазина со сложными правилами (подарки при сумме > X, несовместимые товары, расчет доставки в зависимости от состава). Кейс: Редактор схем/диаграмм (History Middleware).
// features/cart/model/cartSlice.ts
import { createSlice, createEntityAdapter, PayloadAction } from '@reduxjs/toolkit';
const cartAdapter = createEntityAdapter<CartItem>({
selectId: (item) => item.variantId,
sortComparer: (a, b) => a.addedAt - b.addedAt,
});
const cartSlice = createSlice({
name: 'cart',
initialState: cartAdapter.getInitialState({
promoCode: null,
shippingCost: 0,
status: 'idle' as 'idle' | 'calculating' | 'failed',
}),
reducers: {
itemAdded: cartAdapter.addOne, // Нормализация автоматом
itemRemoved: cartAdapter.removeOne,
itemQuantityChanged: (state, action: PayloadAction<{ id: string; qty: number }>) => {
const item = state.entities[action.payload.id];
if (item) item.quantity = action.payload.qty;
},
promoApplied: (state, action: PayloadAction<string>) => {
state.promoCode = action.payload;
// Триггерим пересчет через middleware или listener
},
// ... сложная логика пересчета итогов в extraReducers или listenerMiddleware
},
});
// Экспорт селекторов (мемоизированные)
export const { selectAll: selectCartItems, selectTotal: selectCartItemsCount } = cartAdapter.getSelectors(
(state: RootState) => state.cart
);
// Селектор суммы (пересчитывается только при изменении items/promo)
export const selectCartTotal = createSelector(
[selectCartItems, (state) => state.cart.promoCode],
(items, promo) => calculateTotal(items, promo)
);
Г. Form State: React Hook Form (RHF) + Zod/Valibot
Никогда не храним состояние форм в Redux/Zustand/TanStack Query.
- RHF — неконтролируемые инпуты (performance), валидация,
useFieldArray, интеграция с резолверами схем.
Д. URL State: TanStack Router / React Router v7 / Next.js Search Params
Фильтры, пагинация, сортировка, модалки (deep linking) — в URL. Синхронизация с TanStack Query через queryKey.
3. Архитектурное правило: Разделение ответственности (Separation of Concerns)
┌─────────────────────────────────────────────────────────────┐
│ UI Components │
│ (Только чтение: useQuery, useSelector, useStore) │
└──────────────────────────┬──────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Server State │ │ Global Client │ │ Local/URL │
│ (TanStack │ │ State │ │ State │
│ Query) │ │ (Zustand) │ │ (RHF, URL, │
│ │ │ │ │ useState) │
│ - Кэш │ │ - Theme │ │ │
│ - Инвалидация │ │ - Auth Token │ │ - Form Inputs │
│ - Опт. апдейты│ │ - UI Flags │ │ - Filters URL │
└───────┬───────┘ └───────┴───────┬────────┘───────────────┘
│ │
▼ ▼
┌─────────┐ ┌─────────────┐
│ API │ │ Persistence│
│ Layer │ │ (LocalStorage│
│ (Orval/ │ │ / IndexedDB)│
│ Ky) │ └─────────────┘
└─────────┘
Антипаттерны, которых избегаю:
- Класть
dataизuseQueryв Zustand/Redux — дублирование кэша, рассинхронизация, потеря всех фич TanStack Query. - Класть
theme,isSidebarOpenв TanStack Query — это не серверное состояние, нет ключей запроса, нет инвалидации. - Глобальный Redux для всего подряд — "God Store", невозможно код-сплитить, любое действие ререндерит половину приложения (даже с селекторами, если не мемоизированы правильно).
- Context для частых обновлений (мышь, скролл, анимации) — ре-рендерит всё дерево.
4. Server Components (RSC) & Next.js App Router — нюансы
В Next.js 13+ (App Router) граница проходит иначе:
- Server Components — читают данные напрямую (
await db.query()), нет состояния (stateless). Кэширование черезfetch/unstable_cache. - Client Components (
'use client') — тут живут TanStack Query (для интерактивности: мутации, рефетчинг, оптимистик) и Zustand/Redux (для UI-стейта). - Гидратация: Начальные данные передаются из Server Component в Client Component через пропсы (
initialDataдляuseQuery).
// app/dashboard/page.tsx (Server Component)
import { DashboardClient } from './DashboardClient';
import { getDashboardStats } from '@/entities/dashboard/api';
export default async function Page() {
// Данные приходят с сервера в HTML (RSC Payload)
const stats = await getDashboardStats();
// Пробрасываем как initialData, чтобы TanStack Query не делал лишний запрос на клиенте
return <DashboardClient initialStats={stats} />;
}
// app/dashboard/DashboardClient.tsx ('use client')
import { useQuery } from '@tanstack/react-query';
export function DashboardClient({ initialStats }: { initialStats: Stats }) {
// Используем initialData для гидратации кэша
const { data: stats } = useQuery({
queryKey: ['dashboard', 'stats'],
queryFn: fetchStats,
initialData: initialStats, // Гидратация
staleTime: 1000 * 60,
});
// ...
}
Резюме (TL;DR)
> «Разделяю состояние по природе, а не по библиотеке.
>
> 1. Server State (90% данных) → TanStack Query. Кэш, инвалидация, оптимистик, префетчинг — из коробки. Не пишу ручной fetch в useEffect.
> 2. Global UI State (тема, авторизация, модалки) → Zustand. Без провайдеров, селекторы防止 лишних рендеров, persist middleware для localStorage.
> 3. Complex Client State (корзина с логикой, редакторы, undo/redo) → Redux Toolkit + EntityAdapter. Нормализация, мощные селекторы, DevTools time-travel, middleware.
> 4. Forms → React Hook Form + Zod.
> 5. URL State (фильтры, пагинация) → Router Search Params.
>
> В Next.js App Router: Server Components для начальной загрузки (SEO, скорость), Client Components + TanStack Query для интерактивности. Никакого «глобального стора для всего»».
Вопрос 9. Какой опыт с тестированием: unit, e2e, какие инструменты?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:08:09"/>
Ответ собеседника: неполный. В коммерческих проектах (аутсорс) тесты не писали из-за дедлайнов. Личный опыт: Playwright для e2e (пользовательские сценарии), Storybook для UI-компонентов; Jest не использовал.
Правильный ответ (Senior / Tech Lead уровень)
Честный ответ про отсутствие коммерческого опыта — это нормально для аутсорса, но на уровне Senior/Tech Lead ожидается понимание стратегии тестирования, умение внедрить процесс с нуля и знание современного стека (Jest считается legacy, стандарт — Vitest). Ответ должен демонстрировать, что вы знаете, что, как и зачем тестировать, а не просто названия инструментов.
1. Стратегия: Testing Trophy (Современная альтернатива Пирамиде)
Вместо классической пирамиды (много юнит, мало e2e) современный фронтенд использует Testing Trophy (Kent C. Dodds):
- Основной объем — Integration / Component Tests (тестируем компоненты с зависимостями: провайдеры, роутер, моки API). Дают максимальную уверенность за приемлемое время.
- Вершина — E2E Tests (Playwright) — критичные пользовательские сценарии (Happy Paths): логин, чекаут, ключевой флоу.
- Основание — Unit Tests (Vitest) — только для чистой логики (утилиты, хуки, сторы, парсеры, валидация). Не тестируем детали реализации компонентов.
- Static Analysis (TypeScript, ESLint, Prettier) — ловит ошибки до запуска тестов.
2. Стек инструментов 2024/2025 (Modern Stack)
| Уровень | Инструмент | Зачем / Особенности |
|---|---|---|
| Unit / Integration / Component | Vitest | Нативная поддержка ESM/TS, совместим с Jest API (expect, vi.fn), мгновенный старт (Vite), vi.mock для хуков/модулей. |
| Component Testing (React) | React Testing Library (RTL) | Тестируем поведение пользователя (userEvent), а не детали реализации. screen.getByRole, findByText. |
| API Mocking | MSW (Mock Service Worker) | Перехватывает запросы на уровне Service Worker (браузер) / node:http (Node/Vitest). Один мок для Unit, Component, E2E, Storybook. Контрактный тестирование. |
| E2E | Playwright | Кроссбраузерный (Chromium, Firefox, WebKit), авто-ожидания,trace viewer, параллелизм, шардинг, codegen, UI mode. Лучше Cypress для CI/CD и сложных сценариев (multi-tab, auth). |
| Visual Regression | Playwright Snapshot / Chromatic (для Storybook) | Сравнение скриншотов (pixel-by-pixel) для UI-кита и критических страниц. |
| Component Documentation/Dev | Storybook | Разработка в изоляции, документация пропсов, Interaction Tests (play functions) — это и есть компонентные тесты. Аддоны: a11y, viewport, backgrounds. |
| Coverage | v8 (встроен в Vitest) / Istanbul | Пороги (thresholds) в CI: branches: 80%, lines: 80%, functions: 80%, statements: 80%. Исключаем main.tsx, стили, моки, типы. |
3. Что и как тестирую (Практические паттерны)
А. Unit: Чистая логика (Utils, Hooks, Stores, Schemas)
// lib/utils/currency.test.ts (Vitest)
import { describe, it, expect } from 'vitest';
import { formatPrice, calculateDiscount } from './currency';
describe('currency utils', () => {
it('formats price in RUB correctly', () => {
expect(formatPrice(1500.5, 'RUB')).toBe('1 500,50 ₽');
expect(formatPrice(0, 'USD')).toBe('$0.00');
});
it('calculates discount percentage', () => {
expect(calculateDiscount(1000, 800)).toBe(20);
expect(calculateDiscount(1000, 1000)).toBe(0);
});
});
// hooks/useDebounce.test.ts
import { renderHook, act } from '@testing-library/react';
import { useDebounce } from './useDebounce';
it('debounces value changes', async () => {
const { result, rerender } = renderHook(({ val }) => useDebounce(val, 100), { initialProps: { val: 'a' } });
expect(result.current).toBe('a');
rerender({ val: 'ab' });
rerender({ val: 'abc' });
expect(result.current).toBe('a'); // еще не обновилось
await act(async () => { await new Promise(r => setTimeout(r, 150)); });
expect(result.current).toBe('abc');
});
Б. Integration / Component: Компоненты с контекстом (RTL + MSW + Providers Wrapper)
Не рендерим чистый компонент. Рендерим в окружении приложения.
// test/utils/render.tsx — Кастомный рендер с провайдерами
import { render, RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider } from 'shared/ui/theme';
import { UserProvider } from 'entities/user';
import { worker } from 'test/mocks/browser'; // MSW Worker для браузера (Storybook/Playwright)
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: 0 } }, // Быстрые тесты
});
interface WrapperProps { children: React.ReactNode; }
const AllProviders = ({ children }: WrapperProps) => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<ThemeProvider>
<UserProvider>
{children}
</UserProvider>
</ThemeProvider>
</BrowserRouter>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
export const renderWithProviders = (ui: React.ReactElement, options?: RenderOptions) =>
render(ui, { wrapper: AllProviders, ...options });
// features/auth/ui/LoginForm.test.tsx
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from 'test/utils/render';
import { LoginForm } from './LoginForm';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
// MSW Server для Vitest (Node.js)
const server = setupServer(
http.post('/api/auth/login', async ({ request }) => {
const body = await request.json();
if (body.email === 'test@test.com' && body.password === '123456') {
return HttpResponse.json({ token: 'fake-jwt-token', user: { id: '1', name: 'Test' } });
}
return HttpResponse.json({ message: 'Invalid credentials' }, { status: 401 });
})
);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('LoginForm', () => {
it('shows error on invalid credentials', async () => {
const user = userEvent.setup();
renderWithProviders(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'wrong@test.com');
await user.type(screen.getByLabelText(/password/i), 'wrong');
await user.click(screen.getByRole('button', { name: /login/i }));
await waitFor(() => expect(screen.getByText(/invalid credentials/i)).toBeInTheDocument());
});
it('redirects on success (integration with router)', async () => {
const user = userEvent.setup();
renderWithProviders(<LoginForm />);
await user.type(screen.getByLabelText(/email/i), 'test@test.com');
await user.type(screen.getByLabelText(/password/i), '123456');
await user.click(screen.getByRole('button', { name: /login/i }));
// Проверяем редирект через router (или вызов onSuccess колбэка)
await waitFor(() => expect(screen.queryByRole('button', { name: /login/i })).not.toBeInTheDocument());
// expect(window.location.pathname).toBe('/dashboard'); // если не мокаем router
});
});
В. E2E: Playwright — Только критичные Happy Paths
Не дублируем компонентные тесты. Тестируем реальный бэкенд (staging) или засеeded БД.
// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
import { login } from './helpers/auth';
test.describe('Checkout Flow', () => {
test.use({ storageState: 'playwright/.auth/user.json' }); // Глобальный логин 1 раз
test('user can purchase product with card', async ({ page }) => {
await page.goto('/catalog/product-123');
await page.getByRole('button', { name: /add to cart/i }).click();
// Проверка тоста/сайдбара
await expect(page.getByText(/added to cart/i)).toBeVisible();
await page.getByRole('link', { name: /cart/i }).click();
await expect(page.getByRole('heading', { name: /cart/i })).toBeVisible();
await page.getByRole('button', { name: /checkout/i }).click();
// Заполнение формы (Stripe test card или мок платежного шлюза)
await page.fill('[name="cardNumber"]', '4242 4242 4242 4242');
await page.fill('[name="expiry"]', '12/30');
await page.fill('[name="cvc"]', '123');
await page.getByRole('button', { name: /pay/i }).click();
// Успешная страница заказа
await expect(page.getByText(/order confirmed/i)).toBeVisible({ timeout: 10000 });
const orderId = await page.locator('[data-testid="order-id"]').textContent();
expect(orderId).toMatch(/^ORD-\d+$/);
});
});
// e2e/helpers/auth.ts
import { test as setup, expect } from '@playwright/test';
setup('authenticate as user', async ({ page }) => {
await page.goto('/login');
await page.fill('[name="email"]', process.env.TEST_USER_EMAIL!);
await page.fill('[name="password"]', process.env.TEST_USER_PASSWORD!);
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
Г. Storybook + Interaction Tests (Component Testing в браузере)
// components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
import { within, userEvent } from '@storybook/testing-library';
import { expect } from '@storybook/jest';
const meta: Meta<typeof Button> = {
title: 'UI/Button',
component: Button,
tags: ['autodocs'],
};
export default meta;
export const Primary: StoryObj = {
args: { children: 'Click me', variant: 'primary' },
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
// Можно проверять вызовы onClick через args или моки
},
};
4. Организация процесса в проекте (Tech Lead View)
- CI/CD Pipeline (GitHub Actions / GitLab CI):
- Stage 1: Lint & Typecheck (
npm run lint,tsc --noEmit) — быстрые, параллельно. - Stage 2: Unit/Component Tests (
vitest run --coverage) — параллельно, шардинг по файлам. - Stage 3: Build (
vite build/next build) — артефакты для E2E. - Stage 4: E2E Tests (
playwright test --shard=1/3) — параллельно на нескольких машинах/контейнерах. - Stage 5: Visual Regression (Chromatic / Playwright
toMatchSnapshot) — опционально, на PR в main.
- Stage 1: Lint & Typecheck (
- Coverage Thresholds: Блокируем мерж, если покрытие упало ниже 80% по ветвлениям/строкам для
src/(исключаяmain.tsx,*.stories.tsx,*.test.tsx, моки). - Test Data Management:
- Unit/Component: MSW (мокируем контракты OpenAPI).
- E2E: Seeded Staging DB (скрипт
db:seed:testперед запуском) или Test Containers (Docker с БД для изоляции).
- Flaky Tests Policy: Любой флакинг тест — в Quarantine (отдельный файл/метка
@flaky), фиксим за 1 спринт, иначе удаляем. Не доверяем CI с флакингом. - Code Review Checklist:
- Есть ли тесты на новую логику (хук, утилиту, стор)?
- Есть ли компонентный тест на новую фичу (рендер, интеракция, ошибка)?
- Обновлены ли E2E, если изменился критический флоу?
- Нет ли
waitForс большими таймаутами вместо правильныхfindBy*? - Нет ли тестирования деталей реализации (
toHaveBeenCalledWithна внутренние функции)?
5. Как правильно ответить про «нет коммерческого опыта» (Фрейминг)
> «В текущих коммерческих проектах (аутсорс) практика писать тесты не закрепилась из-за жестких дедлайнов и краткосрочных контрактов. Однако я системно изучаю и применяю современные подходы в пет-проектах и вносил инициативы по внедрению тестирования там, где был Tech Lead:
>
> 1. Настроил стек: Vitest + RTL + MSW для юнит/интеграционных тестов (быстрее Jest, нативный ESM/TS).
> 2. Пишу Component Tests как основной слой (Testing Trophy) — рендерю компоненты с реальными провайдерами (Router, QueryClient, Theme) и мокаю только API через MSW.
> 3. E2E на Playwright — покрываю Happy Paths (Auth, Checkout, Core Flow), использую storageState для пропуска логина, шардинг для скорости в CI.
> 4. Storybook — для разработки UI-кита в изоляции, пишу Interaction Tests (play functions) для проверки поведения компонентов.
> 5. CI/CD: Знаю, как настроить пайплайн с покрытием (thresholds), параллелизацией и борьбой с флакингом.
>
> Готов внедрить эту культуру в команду: начать с настройки инструментов, написать примеры лучших практик (Testing Patterns Wiki), ввести гейты в CI и менторить команду по написанию тестов на новые фичи».
Резюме (TL;DR)
> «Стек: Vitest + React Testing Library + MSW (Unit/Integration/Component), Playwright (E2E), Storybook (Visual/Interaction). > > Стратегия — Testing Trophy: фокус на компонентных тестах с реальным окружением (провайдеры, роутер) и моками сети (MSW). Юнит-тесты только для чистой логики (хуки, утилиты, сторы). E2E — только критичные пользовательские пути. > > В коммерции опыт внедрял с нуля: настройка CI, пороги покрытия, борьба с флакингом, обучение команды. MSW использую как единый источник моков для всех уровней тестов и Storybook».
Вопрос 10. Какие подходы к стилизации компонентов предпочитаете?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:09:17"/>
Ответ собеседника: правильный. Использует CSS Modules, препроцессоры (Sass/Less) и Tailwind CSS; выбор зависит от проекта: для личного сайта — Tailwind (скорость, нет лишних файлов).
Правильный ответ:
Выбор подхода к стилизации — это архитектурное решение, влияющее на производительность рантайма, Developer Experience (DX), поддержание дизайн-системы, bundle size и порог входа для команды. Нет «лучшего» инструмента, есть подходящий под контекст.
1. Классификация подходов и их место в 2024/2025 году
| Категория | Инструменты | Принцип работы | Где применяю |
|---|---|---|---|
| Zero-Runtime CSS-in-JS (Compile-time) | Panda CSS, Vanilla Extract, Linaria, StyleX | Типизированные токены → CSS-файлы на этапе билда. Нет JS-рантайма. | Приоритет №1 для новых продуктовых проектов с дизайн-системой. Type-safe, производительно, работает с RSC. |
| Utility-First (Atomic CSS) | Tailwind CSS v4 (Oxide engine), UnoCSS | Генерация утилит по запросу (JIT). Конфиг на TS/JS. | Быстрый старт, MVP, админки, проекты без уникальной дизайн-системы. v4 решает проблемы конфигурации и производительности. |
| CSS Modules + PostCSS / Lightning CSS | *.module.css, postcss-preset-env, lightningcss | Нативные CSS Variables, скопинг через хеши классов. Стандарт веба. | Легаси, простые лендинги, проекты где команда слаба в JS/TS, или когда нужен максимальный контроль над CSS. |
| Runtime CSS-in-JS | styled-components, emotion, @emotion/css | Генерация стилей в рантайме (или бабеле). | Не рекомендую для новых проектов. Оверхед рантайма, проблемы с React 18 Concurrent / RSC, сложная типизация тем. Только поддержка легаси. |
| CSS-in-JS с атомарным выводом | Tamagui, NativeWind (RN) | Компиляция в атомарный CSS, оптимизация для React Native + Web. | Кроссплатформенные приложения (Expo / React Native Web). |
2. Критерии выбора (Decision Matrix для Tech Lead)
| Фактор | Tailwind / UnoCSS | Panda CSS / Vanilla Extract | CSS Modules + PostCSS |
|---|---|---|---|
| Дизайн-система (Design Tokens) | Конфиг tailwind.config.ts — источник правды. Токены = утилиты. | Type-safe токены (tokens.colors.blue[500]). Валидация на этапе TS. Лучшая DX для DS. | Ручная синхронизация: tokens.json → PostCSS → CSS Variables → vars.css. Больше бойлерплейта. |
| Type Safety | Ограниченная (IntelliSense для классов, но не для значений пропсов). | Полная. css({ color: 'blue.500' }) — ошибка TS, если нет токена. | Нет (только через d.ts генераторы для модулей, слабо). |
| Производительность (Runtime) | 0 KB (только CSS). | 0 KB (только CSS). | 0 KB. |
| Bundle Size (CSS) | Оптимален (только используемые утилиты). | Оптимален (атомарный вывод / рецепты). | Зависит от ручного написания. Риск неиспользуемого CSS. |
| RSC (React Server Components) | Полная поддержка (классы в HTML). | Полная поддержка (CSS извлекается в файлы). | Полная поддержка. |
| Theming (Dark Mode / Multi-brand) | dark: variant, data-theme + CSS Vars. | Встроенные conditions / cssVar конфиги. Мощнее для мультибренда. | Ручная работа с CSS Variables на :root / [data-theme]. |
| Кривая обучения / Онбординг | Низкая (утилиты = CSS свойства). Документация отличная. | Средняя (новый синтаксис css(), cva, recipes, паттерны). | Низкая (чистый CSS). Но нужно знать специфику модулей. |
| Рефакторинг / Удаление мертвого кода | Сложно (классы разбросаны по JSX). PurgeCSS работает, но боится динамических классов. | Легко. Неиспользуемые рецепты/токены = ошибка TS / не попадают в бандл. | Средне. Нужен purgecss или ручной аудит. |
3. Мой стек по умолчанию для новых проектов (2024+)
Вариант А: Продукт с Дизайн-системой / Масштабируемое приложение → Panda CSS (или Vanilla Extract)
Почему:
- Type-Safe Design Tokens: Токены определены в
panda.config.ts→ используются в коде какtokens.colors.brand[500]. Ошибка компиляции, если токен удален/переименован. - Patterns / Recipes: Аналог
cva(class-variance-authority), но нативно в компиляторе.// panda.config.tsexport default defineConfig({theme: { extend: { tokens: { colors: { brand: { 500: { value: '#3b82f6' } } } } },patterns: {button: {description: 'Button styles',properties: ['variant', 'size'],defaultValues: { variant: 'primary', size: 'md' },transform(props) {const { variant, size } = props;return {backgroundColor: variant === 'primary' ? 'brand.500' : 'gray.200',color: variant === 'primary' ? 'white' : 'gray.900',px: size === 'md' ? '4' : '2',py: size === 'md' ? '2' : '1',borderRadius: 'md',fontWeight: 'medium',_hover: { opacity: 0.9 },};},},},});// Компонентimport { button } from 'styled-system/patterns';export const Button = ({ variant, size, children, ...props }) => (<button {...button({ variant, size })} {...props}>{children}</button>);// Button({ variant: 'secondary' }) -> Ошибка TS, если нет в конфиге. - CSS Variables First: Генерирует CSS Variables для токенов → легко переопределять темы через
[data-theme="dark"]без ребилда. - Артефакты: Чистый
.cssфайл +.d.tsтипы. Никакого рантайма.
Вариант Б: Админка / MVP / Внутренний инструмент / Нет уникальной DS → Tailwind CSS v4
Почему v4 (Oxide):
- Нет конфиг-файла (
tailwind.config.jsопционален). Токены определяются в CSS через@theme. - Нативная поддержка CSS Variables (
--color-brand-500). - Молниеносная скорость сборки (Rust-based Oxide engine).
- Идеально для "Utility-First" мышления: верстка в JSX без переключения контекста.
/* globals.css — вся конфигурация здесь */
@import "tailwindcss";
@theme {
--color-brand-500: #3b82f6;
--font-sans: "Inter", sans-serif;
--breakpoint-3xl: 120rem;
}
/* Кастомные утилиты через @utility */
@utility container {
margin-inline: auto;
padding-inline: 1rem;
max-width: 80rem;
}
// Использование
<div className="container mx-auto p-4">
<button className="bg-brand-500 text-white px-4 py-2 rounded-md hover:opacity-90">
Save
</button>
</div>
4. Работа с Design Tokens и Theming (Кросскаттинг)
Независимо от инструмента, архитектура тем должна быть единой:
- Источник правды:
tokens.json(Style Dictionary / Tokens Studio / Figma Tokens) → генерирует код для Panda/Tailwind/Vanilla Extract. - CSS Variables как Runtime API:
:root { --color-bg: var(--color-white); --color-text: var(--color-gray-900); }[data-theme="dark"] { --color-bg: var(--color-gray-950); --color-text: var(--color-white); }
- Provider Pattern (React Context):
// providers/ThemeProvider.tsxexport const ThemeProvider = ({ children, defaultTheme = 'system' }) => {const [theme, setTheme] = useState<Theme>(defaultTheme);useEffect(() => { document.documentElement.dataset.theme = theme; }, [theme]);return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;};
- Нет инлайн-стилей для тем (
style={{ color: theme.color }}) — только классы / атомарный CSS.
5. Антипаттерны, которых избегаю
- Динамические классы в Tailwind/Panda:
className="text-${color}-500"— ломает статический анализ и purging. Только полные строки или утилиты типаcva/recipes. styled-components/emotionв новых проектах: Рантайм-оверхед, проблемы сuseInsertionEffect/ RSC, сложная типизация тем (DefaultThemeaugmentation).- Глобальный CSS без скоупинга (кроме
globals.cssдля reset/tokens/fonts). Все компоненты — изолированные стили. - Смешивание подходов в одном компоненте: Либо только утилиты, либо только
css()/styled(), либо только CSS Modules. Не<div className="tw-class" css={css...} />. - Игнорирование
@layer(Cascade Layers): В чистом CSS / PostCSS использую@layer reset, tokens, components, utilities;для управления специфичностью без!important.
6. Пример структуры файлов (Feature-Sliced Design + Panda/Tailwind)
src/
├── app/
│ ├── providers/ThemeProvider.tsx
│ └── globals.css # @import "tailwindcss" / @import "styled-system/css" + @layer reset, base
├── shared/
│ ├── ui/
│ │ ├── Button/
│ │ │ ├── Button.tsx # Использует button recipe / className
│ │ │ ├── Button.stories.tsx
│ │ │ └── Button.test.tsx
│ │ └── Input/...
│ ├── lib/
│ │ ├── css.ts # Экспорт css, cva, recipes (Panda) / twMerge (Tailwind)
│ │ └── tokens.ts # Реэкспорт токенов для использования в TS логике (градиенты, графика)
│ └── config/
│ └── panda.config.ts # или tailwind.config.ts (если v3)
├── entities/
│ └── Product/
│ └── ui/ProductCard/ProductCard.tsx
└── features/
└── Cart/
└── ui/CartDrawer/CartDrawer.tsx
Резюме (TL;DR для ответа)
> «Выбираю инструмент под масштаб и требования к дизайн-системе:
>
> 1. Panda CSS (Vanilla Extract) — основной выбор для продуктов. Type-safe токены, Patterns (recipes), 0 KB runtime, идеальная работа с RSC, мощный theming. Это «CSS-in-JS нового поколения» без минусов старого.
> 2. Tailwind CSS v4 — для админок, MVP, внутренних инструментов. Скорость разработки, нет конфига, отличная DX для утилитарной верстки.
> 3. CSS Modules + PostCSS (Lightning CSS) — для легаси, простых лендингов, команд без опыта TS/JS-in-CSS.
>
> Ключевые принципы: Design Tokens как Single Source of Truth (генерируются из Figma/JSON), CSS Variables для рантайм-теминга, никакого рантайма для стилей, строгий линтинг классов (Tailwind: tailwindcss-oxide / Panda: TS types), изоляция стилей на уровне компонента.»
Вопрос 11. Какие UI-киты и библиотеки компонентов вы использовали в проектах?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:10:44"/>
Ответ собеседника: правильный. Знает Ant Design, Material UI; работал с headless-решениями: Headless UI, Shadcn UI, Radix UI — голые компоненты, которые стилизовал самостоятельно. Material UI не вызывает сложностей.
Правильный ответ:
Выбор UI-библиотеки — это стратегическое решение, влияющее на bundle size, a11y (доступность), скорость разработки, гибкость дизайна и долгосрочную поддержку. Я разделяю их на три категории и выбираю исходя от зрелости продукта и требований дизайна.
1. Headless UI / Unstyled Libraries (Мой приоритет №1 для продуктов с уникальным дизайном)
Это «золотой стандарт» 2024+ для кастомных дизайн-систем. Вы получаете поведение, a11y (ARIA), управление фокусом, клавиатурную навигацию — самую сложную и дорогую часть. Визуал (CSS) пишете сами под свой дизайн-токены.
| Библиотека | Экосистема | Ключевые фичи | Когда выбираю |
|---|---|---|---|
| Radix UI (Primitives) | React | Самая полная покрытие паттернов WAI-ARIA. Низкоуровневые примитивы (Dialog, Select, Popover, Tooltip, Accordion, Navigation Menu, Hover Card). Разделение на Root, Trigger, Content, Portal. | Дефолтный выбор для сложных интерфейсов (SaaS, дашборды, редакторы). Максимальный контроль над разметкой. |
| React Aria (Adobe) | React / React Native (Web) | Hooks-based API (useDialog, useSelect, useMenu). Работает на уровне хуков, а не компонентов. Лучшая поддержка виртуализации и коллекций (бесконечные списки в селектах). Интеграция с React Stately (state management). | Когда нужна максимальная гибкость разметки (например, нестандартная структура DOM) или кроссплатформенность (React Native Web). |
| Headless UI (Tailwind Labs) | React / Vue | Компонентный API (<Dialog>, <Listbox>, <Combobox>). Проще Radix, «из коробки» дружит с Tailwind. Меньше примитивов (нет Navigation Menu, Date Picker). | Проекты на Tailwind CSS, где нужна простота и скорость, а функционал стандартный (модалки, дропдауны, табы). |
| Ark UI / Panda CSS | Framework-agnostic (React, Vue, Solid, Svelte) | Машинно-читаемые стейт-машины. Интеграция с Panda CSS (recipes/patterns). | Если использую Panda CSS — берем Ark UI для бесшовной интеграции с дизайн-токенами. |
Как я строю Design System на Headless (Паттерн «Wrapper Components»):
// shared/ui/Select/Select.tsx — Обертка над Radix Select
import * as SelectPrimitive from '@radix-ui/react-select';
import { styled } from 'styled-system/jsx'; // Panda CSS / Tailwind / CSS Modules
// 1. Стилизуем примитивы под наш дизайн-токены
const SelectRoot = styled(SelectPrimitive.Root, { base: { width: '100%' } });
const SelectTrigger = styled(SelectPrimitive.Trigger, {
base: {
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
h: '10', px: '3', borderRadius: 'md', borderWidth: '1px',
borderColor: 'gray.300', bg: 'white', _focusVisible: { outline: 'none', ring: 2, ringColor: 'brand.500' },
_disabled: { opacity: 0.5, cursor: 'not-allowed' },
}
});
const SelectContent = styled(SelectPrimitive.Content, {
base: {
overflow: 'hidden', borderRadius: 'md', borderWidth: '1px', borderColor: 'gray.200',
bg: 'white', shadow: 'lg', zIndex: 'dropdown', maxH: '80', w: 'full'
}
});
const SelectItem = styled(SelectPrimitive.Item, {
base: { py: '2', px: '3', outline: 'none', cursor: 'pointer',
_highlighted: { bg: 'brand.50', color: 'brand.700' },
_disabled: { opacity: 0.5 }
}
});
// ... Viewport, ScrollUpButton, Separator, Label, Value, Icon
// 2. Экспортируем составной компонент с нашей типизацией
export const Select = Object.assign(SelectRoot, {
Trigger: SelectTrigger,
Value: SelectPrimitive.Value,
Icon: SelectPrimitive.Icon,
Content: SelectContent,
Viewport: SelectPrimitive.Viewport,
Item: SelectItem,
Label: SelectPrimitive.Label,
Separator: SelectPrimitive.Separator,
// ...
});
// 3. Использование в приложении — чистый DX, полный контроль стилей
<Select.Root>
<Select.Trigger><Select.Value placeholder="Выберите..." /></Select.Trigger>
<Select.Content>
<Select.Viewport>
<Select.Item value="1">Опция 1</Select.Item>
<Select.Item value="2">Опция 2</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Root>
Плюсы: Единый визуальный язык, полная изоляция от сторонних классов, легко мигрировать на другую headless-библиотеку (меняем только shared/ui), типизация пропсов под наш API.
2. Heavy UI Kits (Component Libraries) — Для админок, B2B, MVP, внутренних инструментов
Где скорость доставки > уникальность дизайна. Дизайн-система уже готова, темизация через конфиг.
| Библиотека | Плюсы | Минусы | Мой опыт / Когда юзаю |
|---|---|---|---|
| Ant Design (AntD) v5 | Огромное покрытие паттернов (Table, Form, ProComponents, Charts, Descriptions, Upload, Transfer). ConfigProvider для токенов (CSS-in-JS v5 -> CSS Variables). TypeScript first. | Большой бандл (даже с tree-shaking). Стиль «AntD» узнаваем, сложно сделать «не AntD». Зависимость от @ant-design/cssinjs (рантайм). | Стандарт де-факто для Enterprise админок / B2B панелей. ProTable + ProForm экономят недели разработки CRUD. Знаю внутренности: Form.useForm, Table кастомные рендеры, виртуализация. |
| MUI (Material UI) v5/v6 (Joy UI / MUI X) | Мощная темизация (ThemeProvider, CssVarsProvider — нет рантайма в v6). MUI X Data Grid Pro — лучшая таблица на рынке (виртуализация, экспорт, пивот). Полная a11y. | Исторически тяжелый (v5 Emotion runtime). Joy UI легче, но менее зрела. Цена лицензии за Data Grid Pro. | Проекты с тяжелыми таблицами/гридами (финтех, логистика), где нужен Excel-like опыт. v6 с CSS Variables решает проблемы SSR/продукции. |
| Chakra UI v2 / v3 (Panda CSS based) | Проп-ориентированный API (<Box p={4} bg="brand.500">). Отличная DX. v3 на Panda CSS = 0 runtime, type-safe tokens. | Меньше сложных компонентов (нет продвинутой таблицы, дерева, датапикера из коробки). | Стартапы, MVP, проекты где команда любит prop-driven стили. Легко кастомизировать через theme.extend. |
| Shadcn UI | Не библиотека, а коллекция копипаста. Код у вас в репо (components/ui/). На Radix + Tailwind. Полный контроль. | Нет версии / обновлений (ручной мёрж). Нет сложных компонентов (Data Grid, Charts, Rich Text Editor). Требует знания Tailwind/Radix. | Современные паблик-приложения, лендинги, профили пользователей. Идеально для «своего» вида без боли переопределения CSS. |
3. Специализированные / Нисходящие задачи (Composable Primitives)
Иногда нужен только один сложный компонент, не таскать всю библиотеку.
- TanStack Table (v8) — Headless Table. Только логика (сортировка, фильтрация, пагинация, группировка, виртуализация, расширение строк). Рендеринг — ваш. Must have для любых кастомных таблиц.
- React Hook Form + Zod/Valibot — Headless Forms. Стандарт де-факто.
- React Big Calendar / FullCalendar / Schedule-X — Календари/Слоты.
- ProseMirror / TipTap / Slate / Lexical — Rich Text Editors (Headless cores + UI).
- React Flow / @xyflow/react — Диаграммы/Графы (nodes/edges).
- CM6 (CodeMirror 6) / Monaco Editor — Редакторы кода.
- Recharts / Visx / Tremor / Apache ECharts — Чарты (выбираю по задаче: Visx для кастомных SVG, Tremor для дашбордов на Tailwind, ECharts для сложных интерактивных).
4. Критерии оценки (Checklist) перед добавлением в package.json
- Bundle Size Impact:
bundlephobia.com/size-limitв CI. Headless ~2-5kb/gz, MUI/AntD ~50-100kb/gz (на компонент). - Tree-shaking / Side Effects: Есть ли
"sideEffects": falseвpackage.json? Поддерживает ли ESM? - SSR / RSC Compatibility: Работает ли в Next.js App Router (Server Components)?
- Radix / React Aria / Ark UI — Да (нет
useEffect/useLayoutEffectв корне, только в клиентских обертках). - MUI v6 (CssVarsProvider) / Chakra v3 / AntD v5 (CSS Variables mode) — Да.
- Styled-components / Emotion (v5) / MUI v4 — Проблемы (нужен
Client Componentboundary, сбор стилей).
- Radix / React Aria / Ark UI — Да (нет
- Accessibility (a11y): Проходят ли аудит
axe-core/eslint-plugin-jsx-a11y? Radix / React Aria / MUI / Chakra — Да. Самописные нативные — Риск. - Theming API: CSS Variables (Runtime) vs Compile-time (Panda/Tailwind). CSS Variables = динамические темы без ребилда.
- Maintenance & Community: Частота релизов, открытые Issues, миграции мейджорных версий (AntD v4->v5, MUI v4->v5->v6).
- License: MIT / MIT-like. (MUI X Pro / AG Grid Enterprise — платные).
Резюме (TL;DR для ответа)
> «Выбираю по принципу «Headless First»:
>
> 1. Кастомная дизайн-система / Продукт → Radix UI Primitives (React) или React Aria + Panda CSS / Tailwind / CSS Modules. Пишу обертки shared/ui/* — это наш внутренний UI Kit. Полный контроль, 0kb runtime, type-safe токены, идеальный a11y.
> 2. Админки / B2B / Внутренние инструменты / MVP → Ant Design v5 (лучшие ProComponents для CRUD) или MUI v6 + MUI X Data Grid (если нужны продвинутые таблицы). Экономлю время на стандартных паттернах.
> 3. Паблик-приложения / Лендинги / Быстрый старт → Shadcn UI (код у меня, на Radix+Tailwind) или Chakra UI v3 (prop-driven, Panda CSS).
>
> Специализированные задачи: TanStack Table (таблицы), React Hook Form (формы), TipTap/Lexical (RTE), React Flow (диаграммы).
>
> Важно: Всегда проверяю RSC/SSR совместимость, Bundle Size, a11y и Theming via CSS Variables перед принятием решения. Не таскаю тяжелые библиотеки ради одного компонента (например, не ставлю MUI ради одного DatePicker — беру @radix-ui/react-popover + date-fns / @internationalized/date).»
Вопрос 12. Какие библиотеки для форм и валидации вы применяли?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:11:35"/>
Ответ собеседника: правильный. Предпочитает связку React Hook Form + Zod: позволяет генерировать типы и схему валидации из одного источника, валидировать данные с бэкенда. Раньше использовал Formik + Yup, перешел на Zod.
Правильный ответ:
Стандарт де-факто в современном React-разработке — React Hook Form (RHF) + Zod (или Valibot/ArkType). Эта связка дает производительность (неконтролируемые компоненты, минимум ре-рендеров), type-safety (инференс типов из схемы) и DX (декларативная валидация, интеграция с UI-китами).
1. Почему React Hook Form — архитектурный выбор №1
| Характеристика | React Hook Form | Formik / Redux Form (Legacy) |
|---|---|---|
| Рендеринг | Uncontrolled (ref-based). Ре-рендер только при submit / setError / watch. | Controlled (state-based). Ре-рендер на каждое нажатие клавиши (onChange -> setState). |
| Производительность | O(1) подписок. Идеален для больших форм (100+ полей). | O(N) ре-рендеров. Лагает на больших формах без React.memo / FastField. |
| Bundle Size | ~9 KB (gz). Нет зависимостей. | ~13 KB + зависимости (immer, lodash, prop-types). |
| API | Хуки (useForm, useFieldArray, useWatch). Функциональный стиль. | HOC / Render Props / Hooks. Императивный setFieldValue, setValues. |
| Валидация | Нативная + Schema (zodResolver). `mode: 'onChange' | 'onBlur' |
| Интеграция UI | Controller / useController / useFormContext — обертка за 1 строку. | Field / FastField — пропсы field, form, meta. |
Ключевые фичи RHF, которые экономят часы работы:
register/<Input {...register('name')} />— нативная привязка без оберток.useFieldArray— полноценное управление массивами (добавление, удаление, сортировка, перетаскивание dnd) с валидацией каждого элемента.watch/useWatch— подписка на изменения конкретных полей для условного рендеринга (без ре-рендера всей формы).setValue/setError/clearErrors/trigger— императивное управление для сложных кейсов (автозаполнение, серверные ошибки, зависимые поля).formState: { isSubmitting, isSubmitted, isDirty, isValid, errors }— мемоизированные селекторы, не триггерят рендер лишний раз.
2. Zod — Схемы как Single Source of Truth
Zod (или Valibot / ArkType — более легкие и быстрые альтернативы) решает главную боль: дублирование типов.
// shared/api/schemas/user.ts — ЕДИНЫЙ ИСТОЧНИК ПРАВДЫ
import { z } from 'zod';
// 1. Базовая схема сущности (приходит с бэкенда / уходит на бэкенд)
export const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email({ message: 'Некорректный email' }),
name: z.string().min(2, 'Имя минимум 2 символа').max(50),
role: z.enum(['admin', 'user', 'manager']),
avatarUrl: z.string().url().nullable().optional(),
settings: z.object({
notifications: z.boolean(),
theme: z.enum(['light', 'dark', 'system']),
}).default({ notifications: true, theme: 'system' }),
createdAt: z.string().datetime().transform((str) => new Date(str)), // Трансформация в Date
});
// 2. Инференс типа для всего приложения (API, Store, Components)
export type User = z.infer<typeof UserSchema>;
// 3. Схема для ФОРМЫ (частичная, с кастомными сообщениями, без id/createdAt)
export const UserFormSchema = UserSchema
.omit({ id: true, createdAt: true, avatarUrl: true }) // Убираем серверные поля
.partial({ settings: true }) // Настройки опциональны в форме
.extend({
// Добавляем поля, которых нет в API (confirmPassword, файлы)
confirmPassword: z.string(),
avatarFile: z.instanceof(FileList).optional().refine(
(files) => !files || files.length === 0 || files[0].size < 5 * 1024 * 1024,
'Файл не более 5МБ'
),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Пароли не совпадают',
path: ['confirmPassword'], // Привязка ошибки к конкретному полю
});
export type UserFormData = z.infer<typeof UserFormSchema>;
Продвинутые паттерны Zod:
z.discriminatedUnion— для полиморфных форм (разные поля дляtype: 'individual'vstype: 'company').superRefine— сложная.async валидация (проверка уникальности email на бэкенде черезdebounce).transform/preprocess— нормализация данных до валидации (trim строк, парсинг чисел из строк, приведение дат).catch/default— фоллбэки для опциональных полей.
3. Интеграция: zodResolver + Type-Safe Hooks
// features/user-profile/ui/UserProfileForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { UserFormSchema, type UserFormData } from 'entities/user/api/schemas';
import { useUpdateProfile } from 'entities/user/api/mutations';
import { Button, Input, Select, Checkbox } from 'shared/ui';
export const UserProfileForm = ({ initialData }: { initialData: Partial<UserFormData> }) => {
// 1. Типизация методаов формы из схемы (TypeScript магия)
const form = useForm<UserFormData>({
resolver: zodResolver(UserFormSchema), // Автоматическая валидация
defaultValues: {
name: '',
email: '',
role: 'user',
password: '',
confirmPassword: '',
settings: { notifications: true, theme: 'system' },
...initialData, // Мёржим с данными с сервера
},
mode: 'onChange', // Валидация при изменении (после первого сабмита/блюра)
reValidateMode: 'onChange',
});
const { mutate: updateProfile, isPending } = useUpdateProfile();
const onSubmit = (data: UserFormData) => {
// data уже типизирован и валиден!
// Удаляем confirmPassword перед отправкой на API
const { confirmPassword, avatarFile, ...payload } = data;
updateProfile(payload, {
onSuccess: () => toast.success('Профиль сохранен'),
onError: (error) => {
// 2. Маппинг серверных ошибок (422) в поля формы
if (error instanceof ApiError && error.status === 422) {
Object.entries(error.fields).forEach(([field, messages]) => {
form.setError(field as keyof UserFormData, {
type: 'server',
message: messages[0]
});
});
}
},
});
};
// 3. Условный рендеринг без ре-рендера всей формы
const showNotifications = form.watch((v) => v.settings?.notifications);
return (
<form onSubmit={form.handleSubmit(onSubmit)} noValidate>
{/* RHF register — нативные инпуты */}
<Input
label="Имя"
{...form.register('name')}
error={form.formState.errors.name?.message}
/>
<Input
label="Email"
type="email"
{...form.register('email')}
error={form.formState.errors.email?.message}
/>
{/* Controller для кастомных UI-компонентов (Select, DatePicker, RichText) */}
<Select
label="Роль"
options={['admin', 'user', 'manager'].map(v => ({ value: v, label: v }))}
{...form.register('role')} // Работает, если Select передает value/onChange
error={form.formState.errors.role?.message}
/>
{/* FieldArray для динамических списков (теги, телефоны) */}
<PhoneFieldsArray form={form} />
{showNotifications && (
<Checkbox
label="Email уведомления"
{...form.register('settings.notifications')}
/>
)}
<Button type="submit" disabled={isPending} loading={isPending}>
Сохранить
</Button>
</form>
);
};
// Пример FieldArray (отдельный компонент для инкапсуляции)
const PhoneFieldsArray = ({ form }: { form: UseFormReturn<UserFormData> }) => {
const { fields, append, remove, move } = form.useFieldArray('phones');
return (
<div>
{fields.map((field, index) => (
<div key={field.id} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<Input
{...form.register(`phones.${index}.number`)}
placeholder="+7 (999) 000-00-00"
error={form.formState.errors.phones?.[index]?.number?.message}
/>
<Button type="button" variant="ghost" onClick={() => remove(index)}>Удалить</Button>
</div>
))}
<Button type="button" variant="outline" onClick={() => append({ number: '', type: 'mobile' })}>
+ Добавить телефон
</Button>
</div>
);
};
4. Работа с серверными ошибками (Server-Side Validation Mapping)
Бэкенд возвращает 422 Unprocessable Entity с телом:
{ "statusCode": 422, "message": "Validation failed", "fields": { "email": ["Email уже занят"], "name": ["Слишком короткое"] } }
Паттерн: Кастомный хук useFormWithServerErrors или утилита.
// shared/lib/form/utils.ts
import { UseFormSetError, FieldErrors } from 'react-hook-form';
export const mapApiErrorsToForm = <T extends Record<string, any>>(
setError: UseFormSetError<T>,
apiError: ApiValidationError // Тип ошибки с бэкенда
) => {
if (apiError.fields) {
Object.entries(apiError.fields).forEach(([field, messages]) => {
// Поддержка вложенных полей: "settings.theme" -> setError('settings.theme', ...)
setError(field as keyof T, {
type: 'server',
message: Array.isArray(messages) ? messages[0] : messages
});
});
}
// Глобальная ошибка формы (например, "Форма устарела, обновите страницу")
if (apiError.message && !apiError.fields) {
setError('root', { type: 'server', message: apiError.message });
}
};
5. Альтернативы и когда их применять
| Библиотека | Ниша | Вердикт |
|---|---|---|
| TanStack Form | Headless, framework-agnostic (React, Vue, Solid, Svelte). Fine-grained reactivity (signals). | Сильный конкурент RHF. Лучше для не-React проектов или если нужны Signals. В React экосистеме RHF зрелее, больше интеграций. |
| Valibot / ArkType / TypeBox | Замена Zod. Valibot — модульный, ~1KB, функциональный API. ArkType — самый быстрый, синтаксис 1:1 с TypeScript. | Рекомендую Valibot/ArkType для новых проектов. Zod v4 (в бете) решает проблемы бандла и скорости. Миграция с Zod простая (похожий API). |
| Yup | Legacy. Похож на Zod, но медленнее, хуже TS инференс, мутирующие методы (.required() меняет схему). | Только поддержка старых проектов (Formik). |
| Formik | Legacy. Простой API для маленьких форм. | Не использую в новых проектах. Проблемы с производительностью и TS. |
| Conform | Server Actions / Remix / Next.js App Router. Парсит FormData на сервере, валидирует через Zod/Valibot, возвращает Result с ошибками. Прогрессивное улучшение (работает без JS). | Must have для RSC / Server Actions. На клиенте использую RHF, на сервере — Conform. |
6. Чек-лист Senior-уровня при код-ревью форм
- Нет лишних ре-рендеров? Используется
watch/useWatchвместоformStateв JSX?Controllerтолько где нужно (не нативные инпуты)? - Типобезопасность?
useForm<z.infer<typeof Schema>>? Нетanyвregister/setValue? - Валидация на блюре/сабмите?
mode: 'onBlur'или'onTouched'по умолчанию.'onChange'— только после первого сабмита/блюра (чтобы не ругать пользователя до ввода). - Обработка 422 ошибок?
setErrorвызывается для каждого поля бэкенда. Пользователь видит ошибку под конкретным инпутом. - Оптимистичное UI / Отключение сабмита?
disabled={formState.isSubmitting}. Предотвращение двойного клика. - Доступность (a11y)?
aria-invalid,aria-describedby(id ошибки) на инпутах.role="alert"для ошибок.noValidateна<form>. - Файлы / FileList? Валидация размера/типа до отправки (Zod
refine+FileReaderдля превью). - Сброс формы?
form.reset()после успеха илиform.reset(newDefaults)при смене сущности (редактирование другого пользователя). - Тесты? Юнит-тесты схемы (Zod), интеграционные тесты формы (RTL: ввод, сабмит, ошибки, моки мутаций).
Резюме (TL;DR)
> «Стандарт: React Hook Form + Zod (Valibot/ArkType).
>
> RHF — за производительность (uncontrolled), useFieldArray, watch, Controller, минимальный бандл.
> Zod — за z.infer<Schema> (единый источник типов), transform/refine, zodResolver.
>
> Паттерны: Схемы в shared/api/schemas, инференс типов во все слои, zodResolver, маппинг 422 ошибок через setError, useFieldArray для динамических списков, Controller для UI-кита.
>
> Для RSC/Server Actions — Conform (валидация FormData на сервере).
> Альтернативы: TanStack Form (cross-framework), Valibot/ArkType (bundle size/performance).
>
> Не использую Formik/Yup в новых проектах — устарели архитектурно (controlled, плохой TS, размер).»
Вопрос 13. Как вам наш стек: React, Redux Toolkit, Material UI, React Hook Form?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:12:16"/>
Ответ собеседника: правильный. Изучал вакансию заранее, стек полностью совпадает с опытом, вход в проект будет комфортным.
Правильный ответ:
Это зрелый, промышленный стандарт (Industry Standard) для 엔терпрайз-приложений (B2B, SaaS, админки, дашборды). Стек сбалансирован: покрывает все критические слои (UI, State, Forms, Data Fetching) инструментами с сильным комьюнити, долгим LTS и предсказуемым роадмапом. Вход в проект действительно будет плавным, но есть нюансы, которые отличают «просто пишу код» от «архитектурно веду проект на этом стеке».
1. React 18+ (App Router / Vite) — Фундамент
- Concurrent Features: Использую
useTransitionдля неблокирующего рендера больших таблиц/форм,useDeferredValueдля дебаунса поиска без лагов ввода. - Suspense + Error Boundaries: Обязательная архитектура границ ошибок и скелетонов на уровне роутов/фич.
- Strict Mode: Читаю предупреждения в консоли — они ловят баги с двойным монтированием (cleanup в
useEffect, мьютации в рендере). - Server Components (если Next.js App Router): Четкое разделение: Server Components для данных/SEO, Client Components (
'use client') для интерактивности (MUI, RHF, Redux). Избегаю «Client Component Waterfall».
2. Redux Toolkit (RTK) — Предсказуемое глобальное состояние
- RTK Query — Дефолт для Server State. Не пишу
createAsyncThunk+extraReducersвручную. RTK Query дает: кэширование, инвалидацию тегами (providesTags/invalidateTags), оптимистичные апдейты (onQueryStarted+patchQueryData/updateQueryData), префетчинг,selectFromResultдля селекции только нужных полей (избегаем лишних ре-рендеров). createSlice+ Immer: Мутабельный синтаксис в редюсерах — безопасно и чисто.- TypeScript:
RootState,AppDispatch, типизированные хуки (useAppSelector,useAppDispatch) — настраиваю один раз вstore/hooks.tsи забываю. - Middleware:
rtk-queryлоггер,redux-persist(только для клиентского UI-стейта: тема, сайдбар, черновики форм), кастомные мидлвары для аналитики/WS. - Правило: В стор кладем только Client State (UI, авторизация, черновики, настройки пользователя). Server State — исключительно в RTK Query кэш. Не смешиваем.
3. Material UI (MUI) v5/v6 — Дизайн-система «в коробке»
- Joy UI vs MUI Core: Если проект требует строгого Material Design — MUI Core. Если нужен кастомный бренд, меньше «материальности», меньше бандла — Joy UI (CSS Variables, нет Emotion runtime overhead в v6).
- Темизация:
createTheme+CssVarsProvider(v6) — нет рантайма Emotion в проде, CSS Variables нативно. Темы переключаются мгновенно без ре-рендера всего дерева. - Композиция над кастомизацией: Не пишу
sx={{...}}везде. Создаю Wrapper-компоненты (shared/ui/Button,shared/ui/TableContainer,shared/ui/DataGridWrapper), которые инкапсулируют MUI пропсы и дефолты. Это позволяет:- Сменить библиотеку (на Radix/Shadcn) в одном месте.
- Едино настроить a11y, варианты, размеры.
- Избежать «props drilling» MUI-пропсов через все слои.
- DataGrid (MUI X): Если нужны продвинутые таблицы (виртуализация, группировка, экспорт, редактирование) — покупаю лицензию Pro/Premium. Экономит месяцы разработки. Бесплатная версия (
DataGrid) покрывает 80% нужд. - Производительность:
styled()вместоsxдля часто рендеримых компонентов (статические стили выносятся в CSS). Мемоизация контекстов (ThemeContext,FormControlContext).
4. React Hook Form (RHF) + Zod/Valibot — Формы без боли
- Uncontrolled = Performance. Никаких ре-рендеров на ввод.
zodResolver/valibotResolver: Single Source of Truth для типов и валидации.z.infer<Schema>течет вuseForm<Type>.- Интеграция с MUI:
Controller/useControllerдляTextField,Select,DatePicker,Autocomplete,Switch. Пишу хукuseMuiControllerили обертку<Controller as={<TextField />} />вshared/ui. useFieldArray: Для динамических списков (телефоны, позиции заказа, теги) — стандарт.- Server Errors Mapping: Утилита
setError('field', { message: '...' })вonErrorмутации RTK Query /onSubmit— ошибки бэкенда (422) падают под конкретные инпуты.
5. Архитектурные риски этого стека и как их митигирую (Senior View)
| Риск | Митигация |
|---|---|
| Bundle Size (MUI + Emotion + Redux) | 1. MUI v6 (Joy UI) — CSS Variables, нет Emotion. 2. Tree-shaking: импорт @mui/material/Button, а не @mui/material. 3. babel-plugin-transform-imports / SWC config. 4. Code Splitting: React.lazy для тяжелых фич (отчеты, редакторы, DataGrid Pro). |
| Redux Boilerplate / Over-fetching | 1. RTK Query убирает 90% бойлерплейта. 2. selectFromResult в компонентах — подписка только на нужные поля. 3. Нормализация через createEntityAdapter для сложных коллекций. |
MUI sx проп — ре-рендеры и парсинг в рантайме | 1. styled() API для базовых компонентов (компилируется в CSS/Emotion sheet). 2. sx только для уникальных оверрайдов в месте использования. 3. В v6 — нулевой рантайм для CSS Vars. |
RHF + MUI TextField = Controller boilerplate | 1. Своя библиотека shared/ui/FormFields (<FormInput name="..." />, <FormSelect name="..." />). 2. Автоматическое маппинг error / helperText из formState.errors. |
| Типизация RTK Query + RHF | 1. Экспорчу типы из api.ts: User, UserFormData (Partial/Omit). 2. initData в useForm приходит из useGetUserQuery -> data. Типы совпадают. |
| SSR / Next.js App Router | 1. Provider (Redux, MUI Theme, RHF) — только в Client Component обертке. 2. RTK Query: ssr: true + hydrate на клиенте. 3. MUI: CssVarsProvider + generateCacheKey для изоляции стилей. |
6. Как я вхожу в проект (Onboarding Plan)
- Аудит
package.json/tsconfig/eslint/prettier: Проверяю версии (React 18, RTK 2.x, MUI 5/6, RHF 7), строгий TS (strict: true,noUncheckedIndexedAccess), правила импортов (import/order), запретany. - Store Setup: Проверяю
store/index.ts,store/hooks.ts,store/middleware.ts. Есть лиrtk-queryDevTools? Настроен лиserializationCheckдля несериализуемых данных (Date, File) вignorePaths? - MUI Theme: Аудит
theme.ts— есть лиCssVarsProvider? Вынесены ли токены (colors, spacing, typography) в отдельный файл/пакет для шаринга с дизайнерами (Figma Tokens)? - UI Kit (
shared/ui): Есть ли обертки над MUI? Если нет — приоритет №1: создатьButton,Input,Select,Modal,Table,DataGridWrapperс нашим API. - Form Pattern: Есть ли
shared/lib/form/сzodResolver,useFormдефолтами (mode: 'onTouched'), утилитами маппинга ошибок? - API Layer: Проверяю
api/(RTK Query endpoints). ЕдиныйbaseQueryсprepareHeaders(токен),fetchBaseQueryсcredentials: 'include'или кастомныйbaseQueryWithReauth(refresh token logic). - CI/CD: Линтинг типов (
tsc --noEmit), юнит-тесты (Vitest), e2e (Playwright) в пайплайне.
Резюме (TL;DR)
> «Стек Solid, Enterprise-Ready, Well-Maintained. Я не просто «знаю» эти библиотеки — я знаю их внутреннюю кухню, best practices и подводные камни в продакшене под нагрузкой.
>
> Мой подход на этом стеке:
> 1. RTK Query — единственный источник правды для Server State (кэш, инвалидация, оптимизм).
> 2. Redux Slices — только для Client State (UI, Auth, Drafts).
> 3. MUI (v6/Joy UI) — через CssVarsProvider и Wrapper Components (shared/ui), никаких sx спагетти.
> 4. RHF + Zod/Valibot — через обертки FormField, полная типизация, маппинг 422 ошибок.
> 5. Архитектура — FSD / Modular Monolith, строгая типизация границ (Public API), Code Splitting.
>
> Вход в проект — минимальный. Могу сразу брать задачи по фичам, настраивать инфраструктуру (Store, Theme, API Client, Form Factory) и делать код-ревью архитектурного уровня.»
Вопрос 14. Есть ли у вас вопросы к нам?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:12:52"/>
Ответ собеседника: правильный. На данный момент вопросов нет.
Правильный ответ:
Ответ «вопросов нет» на уровне Senior/Tech Lead воспринимается как упущенная возможность проявить экспертность, заинтересованность и стратегическое мышление. Вопросы — это не просто получение информации, это демонстрация того, как вы думаете, какие риски видите заранее и насколько серьезно относитесь к выбору места работы.
Ниже — чек-лист правильных вопросов, сгруппированных по доменам. На собеседовании достаточно задать 2–3 самых релевантных, но держать в голове нужно весь список.
1. Техническая стратегия и Архитектура (Tech Strategy)
Показывают, что вы думаете не в рамках тикета, а в рамках продукта и долгой жизни кодовой базы.
- «Как выглядит ваш текущий Architectural Decision Log (ADR)? Есть ли процесс принятия архитектурных решений (RFC/ADR), или решения принимаются устно/в PR?»
→ Понимаю зрелость инженерной культуры. Отсутствие ADR — признак хаоса или «архитектуры на коленке». - «Какая стратегия по управлению техническим долгом? Есть ли выделенное время (tech debt sprints, 20% time), как приоритизируете рефакторинг против фич?»
→ Проверка реалистичности сроков и уважения к качеству кода. - «Как организована работа с легаси? Есть ли план миграции на актуальные версии стека (React 18/19, MUI v6, RTK Query v2), или работаем в режиме поддержки?»
→ Понимание контекста: гринфилд / брауфилд / рескью-миссия. - «Как вы подходите к Module Federation / Microfrontends / Monorepo (Nx/Turborepo)? Есть ли планы по масштабированию фронтенд-архитектуры при росте команды?»
→ Видение масштабируемости организации (Inverse Conway Maneuver).
2. Процессы разработки и Качество (Engineering Excellence)
Показывают, что вы приносите культуру качества, а не просто пишете код.
- «Как выглядит Definition of Done? Обязательны ли код-ревью, прохождение CI (lint, typecheck, unit/e2e tests), превью-деплой (Vercel/Netlify/Preview Env) перед мержем?»
→ Базовая гигиена. Если нет — это красный флаг для Senior. - «Какая стратегия тестирования? Есть ли пороги покрытия (coverage thresholds) в CI? Как тестируете интеграции с бэкендом (Contract Testing / Pact / MSW)?»
→ Связь с моим предыдущим ответом про Testing Trophy. Проверка соответствия слов делу. - «Как организован процесс релиза? Feature Flags (LaunchDarkly/Unleash/свои)? Canary/Blue-Green деплой? Роллбэк за минуты?»
→ Уровень DevOps зрелости и безопасность доставки. - «Как вы собираете и используете метрики фронтенда в проде? (Web Vitals, RUM — Datadog/Sentry/LogRocket/Plausible). Есть ли алерты на регрессию LCP/CLS/JS Errors?»
→ Data-driven разработка. Senior отвечает за UX в проде, а не только на дев-машине.
3. Взаимодействие с Бэкендом и Данными (API Contract)
Критично для стека RTK Query / React Hook Form / Zod.
- «Как управляется API-контракт? Есть ли OpenAPI/Swagger/GraphQL Schema? Генерируете ли вы типы/клиент (Orval/Hey API/GraphQL Codegen) автоматически в CI, или пишете руками?»
→ Прямое влияние на мой ежедневный труд (DX) и типобезопасность. Ручной ввод — баги и потерю времени. - «Как решаются проблемы N+1, Over/Under-fetching? Есть ли BFF (Backend for Frontend) слой или GraphQL Federation? Как согласовываете изменения API?»
→ Понимание границ ответственности и боли интеграции.
4. Команда, Культура и Рост (People & Culture)
Tech Lead — это про людей и процессы не меньше, чем про код.
- «Как распределяется владение кодовой базой (Code Ownership)? Коллективное (Collective) / Сильные владельцы (Strong) / Модульное? Как происходит онбординг в незнакомые модули?»
→ Влияет на bus factor и скорость разработки. - «Как выглядит карьерный трек для IC (Individual Contributor) vs Management? Есть ли Staff/Principal уровни? Как проходят Performance Review?»
→ Перспективы роста без ухода в менеджмент. - «Как команда справляется с инцидентами (On-call)? Есть ли ротация, компенсация, постмортемы (Blameless Postmortems)?»
→ Культура ответственности и психологическая безопасность. - «Какие вызовы (challenges) сейчас самые большие у команды/проекта технически? Задачу по рефакторингу X / миграции Y / производительности Z кто будет решать?»
→ Показывает, что я готов решать проблемы, а не просто выполнять таски. Дает контекст на первый квартал.
5. Продукт и Бизнес (Product Mindset)
Senior/Tech Lead должен понимать, зачем мы пишем код.
- «Какие ключевые метрики продукта (North Star Metric, OKRs) на этот квартал? Как моя работа на фронтенде напрямую влияет на них?»
→ Фокус на Outcome, а не Output. - «Как взаимодействие с дизайнерами и ПМ? Есть ли Design System команда? Как попадают дизайны в код (Figma -> Tokens -> Code / Anima / руками)?»
→ Боль фронтендера — разметка. Понимание процесса = меньше боли.
Стратегия ответа на собеседовании (Cheat Sheet)
> Не спрашивайте всё сразу. Выберите 3 вопроса: один Технический/Архитектурный, один Процессный/Качество, один Командный/Бизнес. > > Пример идеального мини-сета: > 1. (Архитектура) «Вижу, что стек современный (RTK Query, RHF, MUI v6). Как вы управляете миграциями и техническим долгом? Есть ли практика ADR для фиксации решений по архитектуре?» > 2. (Процесс) «Как организован CI/CD? Обязательны ли превью-деплои для PR и прогон e2e/контрактных тестов перед мержем в main?» > 3. (Бизнес/Команда) «Какая самая большая техническая проблема сейчас у команды, которую вы хотели бы решить в ближайшие 3–6 месяцев, и как вы видите мою роль в этом?»
Почему «Вопросов нет» — слабый ответ для Senior+
- Отсутствие Due Diligence: Вы собираетесь подписать контракт (трудовой) на годы, не проверив «двигатель» и «пробег».
- Пассивность: Senior — это проактивность. Вопросы = сигнал «Я уже мысленно работаю у вас и вижу зоны улучшения».
- Риск несоответствия: Без вопросов вы не узнаете, что деплой ручной, тестов нет, архитектура монолитная без модульности, а онбординг — «читай код сам». Вы попадете в ловушку.
Итог: На финальной стадии всегда задавайте вопросы. Это ваш последний шанс продать себя как партнера, а не как исполнителя.
Вопрос 15. Вы работали в США (втором месте работы)? Каков был опыт?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:13:02"/>
Ответ собеседника: правильный. Жил во Флориде около года, понравился климат и люди.
Правильный ответ:
Опыт работы и жизни в США (Флорида, ~1 год) дал мне ценный профессиональный багаж, выходящий далеко за рамки «климата и людей». Это был период работы в распределенной команде / для американского продукта, который сформировал мои подходы к коммуникации, процессам и инженерной культуре.
1. Профессиональный контекст и стек
- Роль: [Senior Frontend Engineer / Tech Lead] в команде [размер: 5–8 инженеров].
- Продукт: [SaaS B2B / E-commerce / Fintech] — продукт с высокими требованиями к надежности, безопасности (SOC2/HIPAA) и UX.
- Стек: React, TypeScript, Node.js/Go (BFF), GraphQL/REST, AWS/GCP, Kubernetes, CI/CD (GitHub Actions/GitLab CI), Observability (Datadog/Sentry).
- Методология: Agile/Scrum (2-недельные спринты), RFC/ADR для архитектурных решений, обязательные Code Review, Definition of Done с прогоном e2e и контрактных тестов.
2. Ключевые профессиональные вызовы и решения (Hard Skills)
| Вызов | Мой подход / Результат |
|---|---|
| Работа в таймзоне EST/EDT (UTC-5/-4) из Флориды / удаленно | Настроил асинхронную коммуникацию как дефолт: подробные RFC/ADR, записанные демо (Loom), подробные PR description. Синхронизация (Standups, Planning, Retro) — в удобное окно перекрытия (обычно 16:00–19:00 UTC). Это научило меня писать четкую документацию и не блокировать команду ожиданием звонка. |
| Английский как рабочий язык (Technical English) | Ежедневные standups, планирование, ревью кода, инцидент-менеджмент (on-call), общение с PM/Design/Stakeholders — всё на английском. Уровень: Professional Working Proficiency (C1). Свободно объясняю сложные технические концепции (trade-offs архитектуры, инциденты) нетехническим стейкхолдерам. |
| Инженерная культура (Code Quality & Ownership) | Внедрял/поддерживал практики: Testing Trophy (Vitest + Playwright), Contract Testing (Pact) для микросервисов, Feature Flags (LaunchDarkly) для безопасного роллинга, SLO/SLI для фронтенда (Web Vitals, Error Budget). Культура «You build it, you run it» — участвовал в on-call ротации (PagerDuty), проводил blameless postmortems. |
| Работа с легаси и миграции | Лидировал миграцию с Legacy React (Class Components, Redux Saga, CSS Modules) на Modern Stack (Hooks, RTK Query, Tailwind/Panda CSS, Vite). Стратегия: Strangler Fig Pattern, пофичаная миграция через Module Federation / Monorepo (Nx), параллельный запуск в проде за Feature Flags. |
| Производительность и Core Web Vitals | Оптимизировал LCP/CLS/INP для лендингов и критических путей: Streaming SSR (Next.js), Code Splitting, оптимизация бандлов (bundle analyzer), приоритизация ресурсов (preload/prefetch), виртуализация тяжелых списков (TanStack Virtual). |
3. Soft Skills и Культурный опыт (Soft Skills)
- Прямая и конструктивная обратная связь (Radical Candor): Культура код-ревью и ретроспектив предполагает честное, но уважительное обсуждение проблем («This approach has a scaling issue because...» вместо «It's bad»). Применил это в менторстве джуниоров и код-ревью сеньоров.
- Ownership & Proactivity: Ожидание: инженер сам видит проблему (техдолг, флакинг тест, плохой UX), создает тикет/РФС, предлагает решение и реализует, а не ждет задачу в Jira. Это сдвинуло мой миндсет с «исполнителя» на «владельца продукта».
- Работа с неопределенностью: Часто требования приходят высокого уровня (PRD/One-pager). Инженер участвует в Discovery (Spike), оценке рисков, разбиении на таски и защите оценок перед стейкхолдерами.
- Diversity & Inclusion: Работа в международной команде (US, LatAm, Europe, Asia) — уважаю культурные различия в коммуникации (High-context vs Low-context cultures), таймзонах и подходах к иерархии.
4. Почему это актуально для текущей вакансии
- Готовность к работе в распределенной команде / с заказчиками из US/EU: Понимаю специфику асинхронной работы, английский на уровне свободной технической коммуникации (звонки, доки, слак).
- Best Practices «из первых рук»: Приношу проверенные в высоконагруженных продакшенах практики: ADR, Contract Testing, Feature Flags, Observability-driven development, правильная миграция легаси.
- Менторство и Code Review: Опыт построения процесса ревью, который не блокирует, а учит и повышает качество (конвенции, чек-листы, автоматизация линтеров).
- Адаптивность: Быстро вливаюсь в новые процессы, стеки и домены, так как уже проходил через это при релокации и смене проектов.
Резюме (TL;DR для ответа)
> «Да, жил и работал во Флориде около года. Это был мощный профессиональный апгрейд: работа в распределенной команде на английском для US-продукта (SaaS/B2B). > > Главное, что забрал: > 1. Асинхронная инженерия: Доки (ADR/RFC), записанные демо, качественные PR — основа работы через таймзоны. > 2. Production-grade стандарты: Contract Testing, Feature Flags, SLO/Web Vitals, On-call + Blameless Postmortems, «You build it, you run it». > 3. Миграция легаси: Strangler Fig, Module Federation/Nx, Feature Flags для безопасного перехода на современный стек (RTK Query, RHF, Tailwind/Panda). > 4. Technical English (C1): Свободно веду коллы, пишу спеки, ревьюю код, общаюсь с PM/Design. > > Этот опыт позволяет мне сразу эффективно работать в распределенной команде, вносить зрелые инженерные практики и не требовать «ручного управления» процессами».
Вопрос 16. Почему ушли из США? Были проблемы с визой?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:13:30"/>
Ответ собеседника: правильный. Проблемы с легализацией: требовались следующие шаги (виза талантов или политическое убежище), к ним не был готов.
Правильный ответ:
Уход из США был обусловлен неопределенностью иммиграционного статуса и невозможностью гарантировать долгосрочную легальную работу без значительных рисков и временных затрат, которые не соответствовали моим карьерным планам на тот момент.
Детализация контекста (Senior/Tech Lead уровень):
1. Исходная ситуация
- Переехал по [тип визы, например: B1/B2 / J1 / L1 / H1B — указать фактический тип, если уместно, или обобщить], который не предоставлял прямого пути к Green Card или имел строгие временные ограничения.
- Работал легально (EAD / статус визы), но статус был временным и требовал смены/продления.
2. Оценка путей легализации (Due Diligence) Провел глубокий анализ доступных опций с иммиграционными адвокатами:
- EB-1A / O-1 (Extraordinary Ability / Talent Visa): Требует накопления доказательной базы (премия, публикации, судейство, высокие зарплаты, PR). На момент ухода мой профиль был сильным технически (Senior/Lead), но недостаточен для EB-1A/O-1 без 6–12 месяцев целевой подготовки (письмо рекомендаций, спикинг, статьи). Риск отказа (RFE/Denial) был высок.
- EB-2 NIW (National Interest Waiver): Реалистичнее, но требует времени на сбор петиции и обработки (Priority Date / Visa Bulletin), что создает «пробку» неопределенности на 1.5–3+ года.
- H-1B Lottery: Участвовал/мог участвовать, но лотерея — это азарт, а не планирование карьеры.
- Asylum (Политическое убежище): Рассматривался как последний резерв (Plan Z). Это юридически сложный, длительный (годами), эмоционально изматывающий процесс с негарантированным исходом, который накладывает отпечаток на всю дальнейшую иммиграционную историю. Сознательно решил не идти по этому пути, так как он не соответствует моей профессиональной стратегии и ценностям.
3. Принятое решение (Rational Decision Making)
- Критерий: Стабильность, предсказуемость, фокус на инженерии, а не на бумагах.
- Вывод: Инвестиция времени/денег/нервов в «борьбу за статус» с вероятностью успеха < 100% и горизонтом 2+ года блокирует карьерный рост (смена работы, запуск стартапа, покупка жилья, планирование семьи).
- Действие: Организованный, плановый уход (Voluntary Departure / просто вылет до истечения I-94) без нарушений статуса, без депортаций, с чистой иммиграционной историей (Clean Record).
4. Текущий статус и готовность к релокации
- Нахожусь в [Текущая страна/Город] легально, имею [Вид на жительство / Гражданство / Долгую визу / Цифровую кочевую базу].
- Готов к релокации в [Страна компании / EU / UK / UAE / Remote] по рабочей визе (Highly Skilled Migrant, EU Blue Card, Tech Visa, Global Talent и т.д.).
- Профиль сейчас сильнее (опыт Tech Lead, архитектура, менторство, open source), что значительно повышает шансы на O-1 / EB-1A / Global Talent Visa в будущем, если компания заинтересует долгосрочно.
Ключевые посылки для интервьюера (Red Flags Mitigation):
| Риск интервьюера | Моя митигация |
|---|---|
| «Нелегал / Нарушитель» | Чистая история: ушел вовремя, статус не нарушен, нет банов/депортаций. |
| «Нестабильный / Уйдет снова» | Уход был принудительным из-за системы, а не выбором. Сейчас есть легальный фундамент в [Текущая юрисдикция] и четкий план визы для целевой страны. |
| «Политический беженец / Токсичный» | Не подал на убежище. Принял сложное, но профессиональное решение не использовать этот инструмент. Разделяю личные/политические обстоятельства и инженерную ответственность. |
| «Сложно оформить визу» | Профиль Senior/Tech Lead идеально ложится под Global Talent / Highly Skilled / Blue Card треки. Компании-спонсоры обычно закрывают это за 2–4 недели. |
Резюме (TL;DR для ответа устно):
> «Ушел по личному решению из-за тупикового иммиграционного пути. Была временная виза без гарантированного перехода в Green Card. EB-1A/O-1 требовали года подготовки профиля, H-1B — лотерею, убежище — это не мой путь (этично/профессионально). Принял взвешенное решение: уйти легально, сохранить чистую историю, накачать скиллы до уровня, когда виза будет формальностью (Global Talent / O-1), и вернуться на рынок как сильный Senior/Lead с понятным визовым профилем. Сейчас готов к релокации под спонсорство компании».
Вопрос 17. Где сейчас проживаете?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:14:13"/>
Ответ собеседника: неполный. Ответ не зафиксирован в субтитрах (интервьюер говорит «А, я понял, хорошо» после реплики кандидата).
Правильный ответ:
Место проживания: [Город, Страна] (например: Тбилиси, Грузия / Ереван, Армения / Стамбул, Турция / Белград, Сербия / Варшава, Польша / Лиссабон, Португалия — подставьте актуальное).
Иммиграционный статус / Право на работу:
- [Вид на жительство / Временное проживание / Гражданство / Дипломатический статус / Виза цифрового кочевника].
- Работаю легально: Имею полное право на трудоустройство (как наемный сотрудник / ИП / самозанятый / через B2B-контракт) без необходимости спонсорства визы со стороны работодателя в текущей юрисдикции.
Часовой пояс (Timezone):
- UTC+[X] (например, UTC+4 — Тбилиси/Ереван/Самара; UTC+3 — Стамбул/Москва/Минск; UTC+1 — Белград/Варшава/Берлин зимой).
- Перекрытие с [Таймзона компании/Хаба]: [N] часов (например: «Полный рабочий день перекрывается с CET/CEST (Европа) / EST (Восточное время США)»). Готов к синхронной коммуникации в окне [например, 10:00–19:00 по вашему времени].
Планы по релокации / Визовое спонсорство:
- Вариант А (Готов к релокации): «Открыт к релокации в [Страна офиса / EU / UK / UAE]. Мой профиль (Senior/Tech Lead, 7+ лет опыта, стек React/Go/Cloud) полностью соответствует критериям [EU Blue Card / Highly Skilled Migrant / Global Talent Visa / Tech Visa]. Готов пройти процесс релокации за [2–4 недели / 1–2 месяца] после оффера. Ожидаю поддержку компании (visa sponsorship, relocation package).»
- Вариант Б (Удаленка / B2B из текущей локации): «Планирую оставаться в [Текущая страна] ближайшие [1–2 года / долгосрочно]. Работаю по B2B-контракту (ИП/ООО/sole trader) или через EoR (Deel, Remote, Oyster). Полное перекрытие таймзоны, легальные выплаты, налоги плачу самостоятельно. Спонсорство визы не требуется.»
Техническая инфраструктура для удаленной работы:
- Выделенное рабочее место / доступ к коворкингу (WeWork / локальный аналог).
- Стабильный интернет ([Скорость, например: 500 Мбит/с симметричный], резервный канал 4G/5G).
- Бесперебойное питание (ИБП) / Стабильная электросеть.
Резюме (TL;DR для устного ответа):
> «Проживаю в [Город, Страна]. Имею [ВНЖ / Временку / Гражданство], работаю легально как [ИП / Наемник / B2B], спонсорство визы здесь не нужно. > > Таймзона UTC+[X], перекрытие с вашим офисом [N часов / полный день]. > > [Выберите одно]: > 1. Готов к релокации под спонсорство (Blue Card / Talent Visa), профиль соответствует требованиям. > 2. Работаю удаленно из текущей локации по B2B/EoR, инфраструктура на уровне, таймзона удобна, релокация не планируется.»
Вопрос 18. Как себя оцениваете по градации Junior / Middle / Senior?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:14:21"/>
Ответ собеседника: правильный. Считает себя Middle: сложные архитектурные решения требуют ресерча; признает пробелы в веб-сокетах, микрофронтендах и работе в крупных распределенных командах. Уровень зависит от компании.
Правильный ответ:
Самооценка — это не статичный ярлык, а функция от контекста задачи, зоны ответственности и влияния. В современной индустрии границы размыты, поэтому я оперирую компетенностной матрицей, а не титлом. Мой текущий профиль — Strong Middle / Approaching Senior (Solid Middle с элементами Seniority), и вот как это выглядит по измерениям:
1. Техническая глубина (Hard Skills) — Senior Level в Core Stack
| Область | Уровень | Доказательства / Примеры |
|---|---|---|
| Core React & Ecosystem (Hooks, Suspense, RSC, Performance, Testing) | Senior | Архитектурные решения: выбор стейт-менеджмента (RTK Query vs TanStack), паттерны Compound Components / Render Props / Hooks, оптимизация ре-рендеров (мемоизация, виртуализация), настройка CI/CD, линтеры, архитектурные тесты. |
| TypeScript | Senior | Продвинутые типы (Conditional, Mapped, Template Literals, infer, Branded Types), строгая типизация API (OpenAPI/Zod/Valibot), генерация кода (Orval/Hey API). |
| Архитектура Фронтенда (FSD, Modular Monolith, Microfrontends) | Strong Middle → Senior | Проектировал структуру проектов с нуля (FSD), вводил слой shared/api (MSW/Orval), entities, features. Опыт миграции Legacy → Modern. Микрофронтенды (Module Federation / Webpack 5 / Vite Federation) — есть понимание паттернов (Shared State, Versioning, Runtime Integration), но продуктового опыта выката в проде нет (честно признаю пробел). |
| Backend / Fullstack (Go, Node.js, SQL, Docker, K8s basics) | Middle+ | Пишу BFF / API на Go (Gin/Fiber/Chi), проектирую схему БД (PostgreSQL), пишу миграции, настраиваю Docker Compose / GitLab CI / GitHub Actions. Понимаю CAP-теорему, идемпотентность, Outbox Pattern. Не системный инженер, но закрываю задачи «под ключ». |
| WebSockets / Real-time (Socket.io, Centrifugo, WebRTC basics) | Middle | Интегрировал готовые решения (чаты, нотификации, коллаборация). Понимаю протоколы (WS, SSE, WebTransport), переподключение, балансировку (Sticky Sessions / Redis PubSub). Сложных кастомных протоколов / WebRTC mesh не писал — зона роста. |
2. Инженерные практики (Engineering Excellence) — Senior Mindset
- Testing Strategy: Не «пишу тесты», а строю Testing Trophy (Vitest/RTL + Playwright + MSW + Contract Testing). Внедряю пороги покрытия, борюсь с флакингом, настраиваю шардинг в CI.
- Observability: Внедряю Sentry / LogRocket / OpenTelemetry (RUM), кастомные метрики (Web Vitals, Business Metrics), алерты на ошибки/производительность.
- DX & Tooling: Настраиваю Monorepo (Nx / Turborepo), генераторы кода (Plop / Nx Generators), внутренний UI Kit (Storybook + Chromatic), Husky + Lint-staged + Commitlint.
- Security: CSP, CORS, XSS/CSRF защита, Content Security Policy nonce/hashes, зависимостей (Snyk/Dependabot/OSV Scanner).
3. Soft Skills & Влияние (Impact & Scope) — Transition to Senior
| Компетенция | Оценка | Комментарий |
|---|---|---|
| Ownership (Владение продуктом) | Senior | «You build it, you run it». От идеи/дизайна до продакшена, онколла, постмортемов. Думаю в терминах бизнес-ценности и ROI, а не только «сделать тикет». |
| Mentoring & Code Review | Senior | Регулярно менторю 2–3 инженера (Middle/Junior). Провожу глубокие Code Review (архитектура, производительность, безопасность, читаемость), а не нитпикинг. Ввожу гайдлайны (RFC/ADR процесс). |
| Communication & Stakeholders | Strong Middle | Свободно общаюсь с PM/Design/Backend/Business на английском (C1). Умею переводить «бизнесовые хотелки» в технические предложения с trade-offs. Опыт работы в крупных распределенных командах (50+ инженеров, несколько таймзон, SAFe/LeSS) ограничен — работал в командах до 10–15 человек. |
| Decision Making / Trade-offs | Senior | Принимаю решения на основе данных (бенчмарки, PoC), документирую через ADR. Готов защищать решение или признать ошибку быстро (Fail Fast). |
| Hiring / Onboarding | Middle+ | Участвовал в собеседованиях (System Design, Live Coding, Behavioral), писал онбординг-доки. Не строил процесс найма с нуля. |
4. Честная карта пробелов (Growth Areas — куда расту к Staff/Principal)
- Microfrontends at Scale: Продуктовый опыт Module Federation в высоконагруженном проде (versioning strategy, shared runtime, style isolation, CI/CD для независимых деплоев).
- Large Distributed Systems / Platform Engineering: Построение внутренней платформы разработчика (IDP), Shared Infrastructure, Developer Experience для 100+ инженеров.
- Advanced Real-time / Collaborative Editing: CRDTs (Yjs, Automerge), Operational Transformation, WebRTC SFU/MCU архитектура.
- Strategic Technical Leadership: Управление техническим долгом на уровне организации, бюджетирование рефакторинга, работа с CTO/VP Engineering на стратегии 3–5 лет.
5. Почему «Уровень зависит от компании» — это правда
- В стартапе (Seed/Series A): Я Tech Lead / Senior. Закрываю весь фулл-цикл: инфраструктура, бэкенд, фротенд, мобилька (React Native), CI/CD, найм, общение с инвесторами.
- В корпорации / BigTech (FAANG/Unicorn): Я Senior Software Engineer (L5/L6). Фокус на глубокой экспертизе в домене, менторинге, кросс-командных инициативах, RFC процессах. Здесь мне не нужно настраивать Kubernetes — это делает Platform Team.
- В аутсорсе / Аутстаффинге: Часто ожидают «Senior = делает быстро и качественно задачу за задачей». Я это покрываю, но это не реализует мой потенциал влияния.
Резюме (TL;DR для устного ответа):
> «Не ставлю себе один ярлык. По Core Stack (React/TS/Architecture/Testing/Go) и Ownership/Менторству — я на уровне Senior: проектирую систему, несу ответственность за результат, расту людей, ввожу практики. > > По опыту в специфических масштабах (Микрофронтенды в проде, Команды 50+ чел, CRDT/WebRTC) — Strong Middle, это мои зоны роста. > > Ищу позицию, где смогу применить сильные стороны (архитектура, качество, менторство) и закрыть пробелы на реальных сложных задачах (микрофронтенды / платформа / реал-тайм). Готов подтвердить уровень на System Design и Live Coding».
Вопрос 19. Вы подтверждаете, что оцениваете себя именно с этой стороны?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:14:58"/>
Ответ собеседника: правильный. Да, уточняет: отсутствует практический опыт с веб-сокетами, микрофронтендами и крупными распределенными командами, поэтому пока не Senior.
Правильный ответ:
Да, подтверждаю. Эта самооценка основана на честном аудите зоны ответственности (Scope of Impact) и глубине экспертизы в продакшене, а не на синдроме самозванца или стремлении понизить планку.
Вот почему эта градация справедлива и как я с ней работаю:
1. Разница между «Знаю теорию / Делал в пет-проекте» и «Владел в продакшене»
| Область | Мой текущий уровень | Почему это не Senior в этом конкретном домене | План закрытия (Growth Plan) |
|---|---|---|---|
| WebSockets / Real-time | Middle (Integration Level) | Интегрировал Socket.io / Centrifugo / SSE для чатов и нотификаций. Понимаю протокол, реконнекты, бэкпрешшр, sticky sessions. НЕ делал: кастомные протоколы поверх WS, WebRTC (SFU/MCU), CRDT для коллаборативного редактирования, высокую нагрузку (100k+ соединений на ноде), тюнинг ядра ОС (sysctl, epoll). | Готов взять задачу по реал-тайму под менторством эксперта или расти через пет-проект с нагрузкой (k6/Artillery + профилирование). |
| Microfrontends (Module Federation) | Middle (Architectural Awareness) | Проектировал FSD/Modular Monolith, готов к MFE. Понимаю паттерны: Shared Runtime, Versioning Strategy (SemVer vs Single Version), Style Isolation (CSS Modules / Shadow DOM / Shadow CSS), State Sharing (Custom Events / Broadcast Channel / Shared Redux / Zone.js). НЕ делал: продовый деплой независимых MFE (CI/CD пайплайны), рантайм-ошибки версий, миграцию легаси на MFE без Big Bang, организационные аспекты (Team Topologies). | Изучаю кейсы (Spotify, IKEA, DAZN). Готов пилотировать на изолированной фиче (например, виджет в портале). |
| Large Distributed Teams (50+ eng) | Middle (Process Experience) | Работал в командах 5–12 человек (Feature Teams). Знаю Scrum/Kanban, RFC/ADR, Code Review culture, CI/CD. НЕ имел: опыт SAFe/LeSS/Nexus, координации 5+ команд над одним продуктом (Cross-team dependencies, Integration Sprints, Platform Team interaction as a consumer), масштабирования Code Review / Onboarding процессов. | Читаю Team Topologies, Accelerate. Готов к онбордингу в масштабную организацию — базовые навыки коммуникации и владения фичей есть. |
2. Почему я всё равно претендую на сильную Middle / Senior-позицию?
Потому что Seniority — это не чек-лист технологий, а способность решать бизнес-проблемы в неопределенности и отвечать за результат.
Мои Core Competencies (которые я приношу с первого дня) — на уровне Senior:
- End-to-End Ownership: От «Как это должно работать для юзера?» до «Как это мониторить в проде и как откатить за 30 сек».
- Architectural Decision Making (ADR): Выбор стека (React/Next/Go), паттернов (FSD, RTK Query, Panda CSS), обоснование trade-offs (Build vs Buy, Monolith vs MFE, SSR vs CSR).
- Quality Engineering: Внедрение Testing Trophy, Contract Testing, E2E (Playwright), Observability (Sentry, Web Vitals), Security Headers, CI/CD Gates.
- Mentoring & Code Review: Поднимаю уровень команды через ревью (архитектура, перформанс, безопасность), ввожу линтеры/гайды, провожу тех-интервью.
- Fullstack Pragmatism: Пишу бэкенд на Go (API, Workers, SQL), настраиваю инфраструктуру (Docker, K8s basics, GH Actions/GitLab CI), не блокируюсь на «это не моя область».
- Communication (C1 English): Прямой диалог с PM/Design/Stakeholders, защита технических решений на бизнес-языке.
3. Моя стратегия на собеседовании и в работе
Я не скрываю пробелы — я их контекстуализирую:
- Если задача требует глубокого WebRTC — я скажу: «Я могу спроектировать интеграцию с медиа-сервером (MediaMTX/Janus), написать сигналинг на Go/WS, но по медиа-плайну привьючу эксперта».
- Если нужен Microfrontends — я скажу: «Я спроектирую границы модулей (Bounded Contexts), настрою Module Federation с правильной версионизацией shared deps, но CI/CD для независимых деплоев настроим вместе с Platform Team».
Это позиция Senior-инженера: знать границы своей компетенции, честно о них говорить и уметь делегировать / учиться быстро, не ломая продакшн.
Резюме (TL;DR):
> «Да, подтверждаю. Моя самооценка Strong Middle / Approaching Senior отражает разницу между Core Mastery (React, TS, Arch, Go, Quality, Ownership — уровень Senior) и Niche Production Experience (WebRTC/CRDT, MFE at Scale, Org Scale 100+ — уровень Middle). > > Я не ищу компанию, где «все уже работает», я ищу среду, где мои сильные стороны (архитектура, качество, менторство, фуллстек) дадут максимум ценности, а зоны роста (реал-тайм, микрофронтенды, масштаб орги) можно закрыть на реальных боевых задачах за 6–12 месяцев. Готов доказать Core-навыки на System Design и Live Coding прямо сейчас».
Вопрос 20. Чаще работали в одиночку или во фронтенд-команде?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:15:19"/>
Ответ собеседника: правильный. Преимущественно в команде, но небольшой (2–3 человека). Были периоды соло-работы — сложно, так как ложится ответственность за всё.
Правильный ответ:
Опыт обоих форматов есть, но основной и наиболее продуктивный — работа в кросс-функциональной команде (3–8 инженеров). Соло-работа (Solo / Founding Engineer) — это ценный, но специфический опыт, который дал понимание полного цикла, но не заменяет синергию команды.
1. Работа в команде (Core Experience — 80% времени)
| Контекст | Размер команды | Моя роль | Ключевые практики |
|---|---|---|---|
| Продуктовые команды (Feature Teams) | 4–6 FE + 2–4 BE + QA + PM + Design | Senior FE / Tech Lead FE | FSD Architecture, Code Review (обязательный, гейт в CI), Pair Programming (сложные рефакторинги/онбординг), RFC/ADR для архитектурных решений, Planning Poker, Retro. |
| Платформенная / UI Kit команда | 3–4 FE | Maintainer / Contributor | Semantic Versioning, Changelog, Breaking Changes Policy, Storybook (Component Library), Design Tokens Sync (Figma → Code), Migration Guides для потребителей. |
| Распределенная команда (Remote / Multi-tz) | 5–8 FE (EU + US + Asia) | Senior FE | Async-first коммуникация: подробные PR descriptions, записанные демо (Loom), документация в Notion/Confluence, синхронные коллы только для синка/планирования. Overlap window 3–4 часа. |
Что я привнес в командные процессы (Tech Lead contribution):
- Ввел PR Template с чек-листом (Тесты, Строительство, А11y, Миграции, Доки), сократил время ревью на 30%.
- Настроил Danger.js / GitHub Actions для автоматизации нитпикинга (TODO, console.log, размер PR, конвенции нейминга).
- Провел серию внутренних воркшопов: «Эффективный RTK Query», «Testing Trophy на практике», «Доступность (a11y) — не страшно».
- Менторство: регулярные 1-on-1 с Middle/Junior, составление Individual Development Plans (IDP), помощь в прохождении probation period.
2. Соло-работа / Founding Engineer (20% времени — ранние стартапы, MVP, внутренние инструменты)
Вызовы (Pain Points), которые я решал один:
- Full Ownership: Инфраструктура (Docker, CI/CD, K8s/Serverless), Бэкенд (Go/Node API, DB migrations), Фронтенд (Arch, UI, State), QA (E2E Playwright), DevOps (Monitoring, Logs), Коммуникация с заказчиком/инвесторами.
- Decision Fatigue: Выбор стека без спарринг-партнера. Решал через ADR (Architecture Decision Records) — фиксирую контекст, альтернативы, решение, последствия. Это спасало при возврате к коду через 6 месяцев.
- Bus Factor = 1: Документировал всё:
README(запуск, деплой, переменные),RUNBOOK(инциденты),ARCHITECTURE.md(диаграммы C4). Настроил автоматические деплои и алерты в Telegram/Slack, чтобы знать о проблемах мгновенно. - Time Management: Жесткий тайм-боксинг: 2 дня — инфраструктура/техдолг, 3 дня — фичи. Использовал Shape Up / Kanban вместо Скрама.
Что это дало мне (Senior Value):
- Глубокое понимание всего стека (от Linux/Network до UX).
- Умение приоритизировать беспощадно (RICE/ICE frameworks) — делать только то, что дает ценность бизнесу сейчас.
- Навык быстрой валидации гипотез (Spike -> PoC -> Decision) без долгих митингов.
3. Почему я выбираю команду сейчас (Motivation)
> «Соло-работа научила меня ответственности за всё, но лучшие архитектурные решения рождаются в споре/дискуссии. Code Review от сильного партнера находит баги, которые я пропущу сам. Pair Programming на сложных доменах (Domain Modeling, Concurrency) дает в 2–3 раза лучший дизайн. > > Я ищу команду Senior/Lead-уровня, где: > 1. Есть Code Review Culture (не «LGTM», а глубокий разбор). > 2. Практикуют Pair/Mob Programming для knowledge sharing. > 3. Есть Platform/Infra Team, чтобы я фокусировался на Product Engineering, а не на настройке Kubernetes. > 4. Есть менторы для меня (Staff/Principal Engineers), у кого можно учиться системному мышлению и организационному влиянию».
Резюме (TL;DR):
> «Преимущественно — в кросс-функциональных командах 4–8 человек (Senior/Tech Lead роль). Внедрял инженерную культуру: Code Review, RFC, Testing, CI/CD, менторство. > > Опыт Solo / Founding Engineer есть (MVP, внутренние инструменты) — закрывал Full Cycle: от сервера до UI. Это дало автономность и понимание полного стека, но подтвердило: в команде я эффективнее в 3–5 раз за счет синергии, ревью и фокуса на сильные стороны. > > Сейчас целенаправленно ищу сильную команду для роста до Staff/Principal уровня через сложные системные задачи и менторство».
Вопрос 21. Имеется в виду именно фронтенд-команда, а не общая?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:15:26"/>
Ответ собеседника: правильный. Да, именно фронтенд. Соло-работа давала допнагрузку: процессы, менторинг джуниоров, ответственность за прод.
Правильный ответ:
Да, именно фронтенд-команда (Frontend Chapter / Guild / Team). Но важно уточнить контекст: я всегда работал в кросс-функциональных продуктовых командах (Feature Teams / Squads), где фронтенд-разработчики сидят вместе с бэкендом, QA, PM и дизайнерами за одним столом (виртуальным), а не в изолированном «отделе фронтенда», который просто получает задачи откуда-то извне.
1. Структура команд, в которых я работал (Team Topologies)
| Тип команды | Состав | Взаимодействие FE внутри | Моя роль в FE-сообществе |
|---|---|---|---|
| Feature Team (Product Squad) | 2–3 FE, 2–3 BE, 1 QA, 1 PM, 1 Designer | Ежедневно: планирование, рефайнмент, пара-программинг FE+BE (контракты API), код-ревью только внутри FE. | Tech Lead FE / Senior FE: отвечаю за качество FE-кода, архитектуру фичи, менторинг FE-коллег, синхронизацию с BE по контрактам (OpenAPI/GraphQL). |
| Platform / Enabling Team (UI Kit / Design System) | 3–4 FE (нет BE/QA/PM постоянных) | Внутренний продукт: консьюмеры — другие FE-команды. Работа через RFC, SemVer, Migration Guides, Office Hours. | Maintainer / Core Contributor: разработка компонентов, токенов, документации (Storybook), поддержка консьюмеров. |
| Solo / Founding Engineer | 1 FE (я) + 1–2 BE + PM/Designer (частично) | Полная изоляция FE: сам проектирую API с BE, сам верстаю, сам тестирую, сам деплою. | Full Ownership: все FE-решения, настройка тулинга, CI/CD, онбординг будущих FE. |
2. Почему «допнагрузка» в соло — это не только про код (Senior View)
В соло-режиме на FE ложатся все ответственности, которые в команде распределяются:
| Область ответственности | В команде (распределено) | В соло (на мне) | Как я это масштабировал/автоматизировал |
|---|---|---|---|
| Архитектура / RFC | Коллективное обсуждение, ADR | Только я — пишу ADR для себя и истории. | Шаблон ADR в репе, обязательный для любых решений > 4ч работы. |
| Code Review | Парный / Групповой | Сам себе CR (опасно). | Обязательный CI Gate: Линты, Типы, Тесты (Unit/E2E), Bundle Size, А11y, Dependency Scan. PR в main только через Squash Merge. |
| Менторинг / Онбординг | Senior ментирит Middle/Junior | Нет времени / Нет кого учить. | Документация: ONBOARDING.md, ARCHITECTURE.md, RUNBOOK.md, видео-гайды (Loom). Готовность к найму за 1 неделя. |
| Процессы (Planning, Retro, Sync) | Scrum Master / PM ведут | Сам фасилитирую или минимизирую (Kanban/Shape Up). | Shape Up / Kanban: нет спринтов, есть аппетит (6 недель) и ставки. Ретро — раз в квартал сам с собой (Post-mortem). |
| Инфраструктура / DevOps | Platform Team / DevOps | Сам: Docker, CI/CD (GH Actions/GitLab), K8s/Helm/Serverless, DNS, SSL, Monitoring. | IaC (Terraform/Pulumi) для инфры, готовые шаблоны реп (Boilerplate), Renovate/Dependabot для депсов. |
| Ответственность за Прод (On-call) | Ротация в команде | 24/7 (или рабочее время). | Sentry/LogRocket алерты в Telegram, Runbooks для инцидентов, Feature Flags (Unleash/LaunchDarkly) для быстрого отката без деплоя. |
3. Отношение к «Фронтенд-команде» как к сообществу практик (Chapter / Guild)
Даже работая в Feature Team, я считаю Frontend Chapter (Гильдию) критически важной структурой для компании:
- Единый стандарт: ESLint/Stylelint/Prettier конфиги, TSConfig, Git Hooks (Husky), CI Pipeline шаблоны — поддерживаем централизованно в
shared-configsрепе. - Технологический радар: Решаем вместе: «Переходим на React 19 / Next 15 / Tailwind v4 / Bun?», пилотируем на одной команде, раскатываем остальным.
- Кадровый бренч: Участвую в найме (Screening, Tech Interview, Live Coding), формирую требования к грейдам (Grade Matrix: Junior → Middle → Senior → Staff).
- Knowledge Sharing: Внутренние конференции (Tech Talks), RFC Review Club, библиотека паттернов (Internal Wiki).
Резюме (TL;DR):
> «Да, речь именно о фронтенд-сабкоманде внутри кросс-функционального сквада (обычно 2–3 FE). > > Опыт соло дал понимание Full Cycle Ownership: я могу поднять инфраструктуру, настроить CI/CD, спроектировать API, навести порядок в процессах и выкатить прод один. Но это неэффективно долгосрочно из-за Bus Factor и когнитивной нагрузки. > > В команде я фокусируюсь на Technical Leadership во фронтенде: архитектура фич, качество кода (Code Review, Testing), менторинг FE-коллег, DX (тулинг, UI Kit), синхронизация с BE/Design. Ищу среду, где сильный FE-챕тер усиливает друг друга, а не просто параллельно пишет код».
Вопрос 22. Какая именно дополнительная нагрузка возникает, когда вы единственный фронтенд-разработчик?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:15:58"/>
Ответ собеседника: правильный. Описал кейс: был вторым фронтендом, потом джуниор ушел — стал единственным. Ложилось: код-ревью, деплой, мониторинг прод, общение с PM/дизайнером, ревью макетов перед передачей джуниору, созвоны с джуниорами, релизы ночью.
Правильный ответ:
Ситуация «Bus Factor = 1» после ухода коллеги — классический стресс-тест для инженера. Нагрузка здесь не просто «больше работы», а смена контекста (Context Switching) между ролями, которые в здоровой команде разделены между людьми или процессами.
Вот структурированный разбор этой нагрузки и Senior-стратегии выживания/масштабирования в таких условиях:
1. Карта нагрузки: Роли, которые «налетают» на Solo FE
| Роль / Ответственность | Что это значит на практике | Цена ошибки |
|---|---|---|
| Tech Lead / Architect | Выбор стека, архитектура (FSD/Modules), RFC/ADR, техдолг, рефакторинг. Никто не поправит архитектуру в PR. | Архитектурный тупик через 6 месяцев — полная переработка. |
| Senior Developer | Сложные фичи, производительность, безопасность, интеграции (API, WebSockets, SDK). | Баги в проде, уязвимости, плохой UX. |
| Code Reviewer / Quality Gate | Сам себе Code Review. Нет «свежего взгляда». Риск слепоты к своим ошибкам (Blind Spots). | Пропущенные баги, нарушение конвенций, ухудшение читаемости. |
| DevOps / Release Engineer | CI/CD (GitHub Actions/GitLab CI), Docker, K8s/Serverless, Environments (Dev/Stage/Prod), Secrets, Rollback strategy. | Сломанный прод, утечка секретов, долгие деплои, невозможность откатиться. |
| SRE / On-Call | Мониторинг (Sentry, Datadog, LogRocket), алерты, инцидент-менеджмент, постмортемы. Ночные вызовы (как вы верно заметили). | Даунтайм часов, потеря данных, репутационные риски. |
| QA / Test Engineer | Написание Unit/Integration/E2E (Playwright), ручное тестирование, регресс. | Регрессии в критичных флоу (оплата, авторизация). |
| Product Manager Proxy | Уточнение требований, приоритизация багфиксов vs фичей, управление ожиданиями стейкхолдеров. | Строим не то, что нужно бизнесу. |
| Designer Proxy / Design System Maintainer | Ревью макетов (Figma) на реализуемость, доступность (a11y), консистентность, разбиение на компоненты. | Пиксель-перфект баги, недоступный UI, дублирование компонентов. |
| Mentor / Onboarding (если нанимают) | Обучающий трек, код-ревью джуниора, объяснение архитектуры, психологическая поддержка. | Джуниор уходит в провал или уволится через месяц (как и было). |
2. Почему «Релизы ночью» и «Созвоны с джуниорами» — это симптомы сломанного процесса, а не героизм
- Релизы ночью = Отсутствие Feature Flags и стратегии Trunk-Based Development / Canary Deploy. Если фича не готова — она не должна попадать в
main/releaseветку. Релиз должен быть скучным действием днем: «Нажал кнопку Merge -> Автотесты -> Автодеплой -> Проверил метрики -> Закрыл ноутбук». - Созвоны с джуниорами вместо документированного онбординга = Потеря вашего времени (самого дорогого ресурса). Джуниор должен учиться по
ONBOARDING.md,ARCHITECTURE.md, видео-гайдам и задачам с хорошимиDescription/Acceptance Criteria, а не по живым созвонам.
3. Стратегия выживания и масштабирования (Как я это решал / решал бы сейчас)
А. Автоматизация в качестве «Второго инженера» (Automation First)
Настраиваю CI Pipeline как строгого ревьюера, который не устанет и не пропустит плохой код:
# .github/workflows/ci.yml (Суть)
jobs:
quality-gate:
steps:
- Lint (ESLint + Stylelint + TypeScript strict)
- Test (Vitest --run --coverage --thresholds=80/80/80)
- E2E (Playwright --shard=1/3) # Параллельно
- Bundle Analysis (webpack-bundle-analyzer / vite-bundle-visualizer) -> Fail if > Budget
- A11y (axe-core / playwright-axe)
- Dependency Check (npm audit / osv-scanner / dependabot)
- Build (Production build must succeed)
Результат: Мёрж в main = Готово к проду. Нет ручного «апррува».
Б. Feature Flags & Trunk-Based Development (Убираем «Релизы ночью»)
- Вся новая функциональность за флагами (
unleash,launchdarkly,configcatили самописный на Redis/ConfigMap). mainвсегда деплойmain=production-ready.- Релиз = Флип флага (миллисекунды, днем, без деплоя). Роллбэк = Флип флага обратно.
В. Документация как масштабирование знаний (Bus Factor Proof)
Обязательные артефакты в репе (Markdown, рядом с кодом):
ARCHITECTURE.md— C4 диаграммы (Mermaid.js), ключевые решения (ADR), данные потоки.ONBOARDING.md— Запуск за 15 мин (docker compose up/devbox shell), ссылки на ключевые файлы, контакты.RUNBOOK.md— «Что делать, если...» (Sentry алерт, БД упала, память течет, как откатить миграцию).DESIGN_SYSTEM_GUIDE.md— Как пользоваться UI Kit, токенами, как добавить компонент.
Г. Найм и Онбординг (Решение проблемы «Джуниор ушел»)
- Не нанимаю Джуниоров в соло. Риск слишком высок. Ищу Strong Middle / Senior, который может взять владение половиной кодовой базы через 2 недели.
- Если бюджет только на Джуниора — не ухожу в соло, договариваюсь о контракте с аутстаффером/консультантом на 20h/week для Code Review и архитектурного надзора до выхода джуниора на автономную работу.
Д. Границы и управление ожиданиями (PM/Business)
- Провожу еженедельный Sync с PM: «Вот Capacity (например, 30ч на фичи / 10ч на техдолг / 5ч на инциденты). Вот приоритезированный бэклог. Что режем?».
- Ввожу WIP Limits (Kanban): Не более 2-х задач в «In Progress». Блокер = стоп линии.
4. Чек-лист для интервьюера: «Как я буду работать у вас, если останусь один?»
| Ситуация | Мой подход |
|---|---|
| Срочный хотфикс в проде | Фикс -> Тест локально -> Push -> CI (авто) -> Merge -> Feature Flag ON -> Мониторинг 15 мин -> Flag OFF если что. Без ночных деплоев. |
| Нужно ревью макета в Figma | Асинхронно: комментарии в Figma + короткое Loom-видео (5 мин) с вопросами к дизайнеру/PM. Не жду созвона. |
| Джуниор застрял на задаче | 1. Проверяю, есть ли в задаче Acceptance Criteria. 2. Даю ссылку на похожий паттерн в кодовой базе / доку. 3. 15-мин Pair Programming (только если заблокирован > 1 часа). |
| Техдолг давит | Выделяю 20% времени (пятница / 1 день в спринте) строго под рефакторинг/апгрейды. Заводим тикеты в бэклог с лейблом tech-debt, приоритизируем по RICE. |
| Инцидент в 3 часа ночи | Алерты приходят в Telegram/Slack (Sentry + UptimeRobot). Runbook подсказывает действие. Если критическое — чиню/роллбэк. Если нет — акимулирую до утра. Постмортем утром. |
Резюме (TL;DR)
> «Нагрузка соло — это не «кода стало больше», а комбинаторный взрыв ролей: Tech Lead + Senior + QA + DevOps + SRE + PM Proxy + Mentor. > > Мой подход — убрать человеческий фактор из рутины через автоматизацию (CI, Feature Flags, Observability, Docs) и жесткое управление приоритетами (WIP Limits, Capacity Planning). > > Опыт с ушедшим джуниором научил: в соло не беру джуниоров без внешнего ментора/консультанта. Это гарантированный провал для обоих. Ищу либо сильного парня (Middle+), либо команду, где есть код-ревью и разделение ответственности».
Вопрос 23. Как вам такая структура: вы единственный фронтенд, есть бэкенд, девопс, дизайнер, продукт-менеджер (фаундер)?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:17:56"/>
Ответ собеседника: правильный. Интересно, уточнил детали проекта (платформа для найма), готов рассмотреть оффер при совпадении условий.
Правильный ответ:
Это классическая структура ранней стадии (Seed / Series A) / Feature Team — «Продуктовая команда с полным набором компетенций». Для Senior/Tech Lead это идеальная точка входа для максимального влияния, но она несет специфические риски, которые нужно продиагностировать на собеседовании (что вы и сделали, уточнив домен).
Ниже — разбор этой структуры через призму Senior/Tech Lead ответственности.
1. Сильные стороны (Pros) — Почему это круто
| Фактор | Оценка | Комментарий Senior-уровня |
|---|---|---|
| Автономность и Ownership | ⭐⭐⭐⭐⭐ | Вы — единственный эксперт по FE. Никто не будет перекрывать ваши решения. Вы определяете стек (React/Next, RHF, TanStack, Panda/Tailwind), архитектуру (FSD), стандарты кода, CI/CD для фронта. Это «Зеленое поле» для внедрения лучших практик с первого дня. |
| Прямая связь с ценностью (PM = Founder) | ⭐⭐⭐⭐⭐ | Нет «испорченного телефона» через менеджеров-посредников. Вы видите бизнес-метрики, понимаете Why, можете предлагать What и How. Быстрые решения: «Давай сделаем так, это даст конверсию +5%». |
| Наличие DevOps | ⭐⭐⭐⭐ | Критический плюс. Вы не тратите 20-30% времени на Kubernetes, CI/CD, инфраструктуру, секреты, мониторинг. Вы пишете Dockerfile и deploy.yml, а инфраструктура работает. Фокус на Product Engineering. |
| Наличие Дизайнера | ⭐⭐⭐⭐ | Вы не рисуете UI в Figma сами. Есть источник правды (Design System / Figma Library). Ваша задача — Developer Experience при имплементации (Tokens, Components, A11y), а не изобретение визуала. |
| Бэкенд-партнер | ⭐⭐⭐ | Есть с кем согласовать OpenAPI/GraphQL контракт. Можно внедрить Contract-First Development (Orval/Hey API -> TS types + Zod + MSW + TanStack Query). |
2. Риски и «Красные флаги» (Cons & Risks) — Что нужно проверить
| Риск | Вопросы для диагностики (Due Diligence) | План митигации |
|---|---|---|
| Bus Factor = 1 (Frontend) | * «Какой план найма второго FE?»<br>* «Есть ли бюджет на аутстафф/консультанта для код-ревью на первых порах?»<br>* «Как организован On-call для FE-инцидентов (Sentry алерты ночью)?» | Договориться о External Code Review (контракт с сильным Senior/Staff 4-8h/week) до найма. Настроить Feature Flags для безопасного роллбека без деплоя. Документация (ARCHITECTURE.md, RUNBOOK.md) — приоритет №1. |
| PM = Founder (Микроменеджмент / Хаос приоритетов) | * «Как формируется бэклог? Есть ли Roadmap на квартал?»<br>* «Как принимаются решения о скоупе? Есть ли процесс RFC/Spike?»<br>* «Есть ли защита от scope creep?» | Внедрить Shape Up / Kanban с WIP limits. Четкий Definition of Ready (DoR) для задач. Еженедельный планирование (30 мин) + Demo (30 мин). Founder должен доверять технические решения вам. |
| Техдолг / Скорость vs Качество | * «Какое покрытие тестами ожидается?»<br>* «Есть ли время на рефакторинг / апгрейды (React 19, Next 15)?» | Заложить 20% Capacity на Tech Excellence (Tech Debt Friday / Gold Cards). Настроить Quality Gates в CI (Coverage, Bundle Size, A11y, Lint) — незыблемые. |
| Дизайн-система / Консистентность | * «Есть ли UI Kit / Design Tokens в Figma?»<br>* «Как дизайнер передает макеты? (DevMode, Anima, руками?)» | Если нет DS — первый квартал: выстроить shared/ui (Radix/Shadcn + Panda/Tailwind), токенизировать цвета/шрифты/спейсинг. Автоматизировать синк Figma -> Code (Style Dictionary / Tokens Studio). |
| Масштабирование команды | * «Планируется ли найм еще 1-2 FE в ближайшие 6-12 мес?»<br>* «Как будет выглядеть онбординг новичков?» | Подготовить Boilerplate / Starter Kit (Next.js/ Vite + TS + ESLint + Prettier + Husky + CI + TanStack Query + RHF + Zod + UI Kit + Storybook). Новичок должен делать первый PR в день 1-2. |
3. Специфика домена: «Платформа для найма» (HR Tech / Recruitment)
Это накладывает отпечаток на технические требования. Как Senior, я сразу смотрю на:
- Сложные формы и валидация (анкеты кандидатов, профили компаний, многошаговые воронки).
- Стек: React Hook Form + Zod/Valibot (обязательно).
useFieldArrayдля динамических секций.
- Стек: React Hook Form + Zod/Valibot (обязательно).
- Real-time / Уведомления (чат с кандидатом, статус заявки, календарь собеседований).
- Стек: WebSockets (Socket.io / Centrifugo / Ably) + TanStack Query (Optimistic Updates). Или Server-Sent Events (SSE) для простых уведомлений.
- Поиск и фильтрация (Elasticsearch / Algolia / Meilisearch на бэке).
- Фронт: Debounced Search, Infinite Scroll / Virtualized Lists (
@tanstack/react-virtual), URL State Management (Nuqs / Search Params) для шарируемых ссылок фильтров.
- Фронт: Debounced Search, Infinite Scroll / Virtualized Lists (
- Файлы / Аватары / Резюме (PDF, DOCX).
- Фронт: Uppy / Dropzone -> Presigned URL (S3) -> Прогресс бар -> Превью PDF (
pdf.js).
- Фронт: Uppy / Dropzone -> Presigned URL (S3) -> Прогресс бар -> Превью PDF (
- Календарь / Слоты времени (Cal.com / Calendly-like функционал).
- Сложность: Таймзоны (UTC хранение, TZ Database на фронте), рекуррентные события, конфликты. Библиотека:
@schedule-x/calendarили кастомное решение.
- Сложность: Таймзоны (UTC хранение, TZ Database на фронте), рекуррентные события, конфликты. Библиотека:
- Аналитика / Дашборды для HR (конверсия воронки, time-to-hire, источники).
- Стек: TanStack Query + Charting (Recharts / Tremor / Visx). Серверная агрегация (BE), фронт — только визуализация.
- Доступность (a11y) и SEO (публичные страницы вакансий, лендинги).
- Next.js App Router (SSR/ISR) для SEO вакансий. Semantic HTML, ARIA только где нативного недостаточно.
next-sitemap.
- Next.js App Router (SSR/ISR) для SEO вакансий. Semantic HTML, ARIA только где нативного недостаточно.
4. Мой план действий на первых 90 дней (30/60/90 Plan)
Дни 1-30: Фундамент и Доверие (Foundation & Trust)
- Тех. Аудит: Прогоняю код через
eslint --fix,tsc --noEmit, проверяюbundle-analyzer, настраиваю Husky + Lint-staged + Commitlint. - CI/CD: Настраиваю Quality Gates (Typecheck, Unit Tests, E2E Smoke Playwright, Bundle Size Limit, A11y Audit). Деплой в Preview (Vercel/Netlify/Cloudflare Pages) на каждый PR.
- API Contract: Внедряю Orval / Hey API генерацию из OpenAPI. Убираю ручные типы.
- UI Kit Foundation: Настраиваю Panda CSS / Tailwind + Radix UI / Shadcn в
shared/ui. Выношу токены из Figma (Style Dictionary). - Документация:
ARCHITECTURE.md(C4),ONBOARDING.md,RUNBOOK.md(инциденты). - Quick Wins: Закрываю 2-3 «больных» бага (производительность списка, утечка памяти, сломанная форма).
Дни 31-60: Фичи и Качество (Features & Quality)
- Ключевая фича: Внедряю Real-time уведомления / Чат (WebSockets + Optimistic UI).
- Testing Strategy: Покрываю Critical Paths (Auth, Onboarding, Payment/Subscription, Create Vacancy) E2E тестами (Playwright). Unit тесты только для утилит/хуков/схем (Vitest).
- Performance: Настраиваю Web Vitals (LCP/INP/CLS) мониторинг (Sentry / Vercel Analytics / Datadog RUM). Оптимизирую LCP для публичных страниц вакансий (ISR/SSR).
- Design System v1: Выкатываю базовый набор компонентов (Button, Input, Select, Modal, Table, Toast, Tooltip) в Storybook. Документирую использование.
Дни 61-90: Масштаб и Стратегия (Scale & Strategy)
- Найм / Онбординг: Если открыта вакансия — готовлю Starter Kit для новичка. Первая задача новичка — добавить компонент в UI Kit / написать тест.
- Architecture Decision: RFC по миграции на Next.js App Router (RSC) или TanStack Start (если на Vite/SPA), если это даст SEO/Performance выгоду.
- Platform Features: Внедряю Feature Flags (Unleash / Flagsmith) для безопасного роллинга больших фич (например, новый билдер воронок).
- Analytics: Внедряю структурированную аналитику (PostHog / Amplitude / Mixpanel) через единый
analytics.tsпровайдер. - Retro: Провожу техническую ретроспективу с командой: что больно, что работает, план на следующий квартал.
5. Чек-лист оффера (Deal Breakers / Must Haves)
Прежде чем сказать «Да», я проверю соответствие этим критериям:
| Критерий | Must Have | Nice to Have |
|---|---|---|
| Стек | React 18+, TS Strict, Node 20+, Modern BE (Go/Node/Python) | Next.js 14+ (App Router), Bun |
| Инфраструктура | DevOps на штате, Kubernetes / Serverless, CI/CD (GH Actions/GitLab), Observability (Sentry + Logs + Metrics) | Feature Flags Service, Preview Deployments |
| Процессы | Git Flow / Trunk Based, Code Review (обязательный), Issue Tracker (Linear/Jira/GH Issues), Async-first коммуникация | Shape Up / Kanban, RFC Process |
| Дизайн | Figma Library, Design Tokens, Designer доступен для спарринга | Design System в Figma (Variables, Auto Layout) |
| Компенсация | Market Rate (Senior/Lead) + Equity (Options/RSU) если Startup | Relocation Support, Education Budget, Conference Days |
| Культура | Blame-free Postmortems, Psychological Safety, Trust in Technical Decisions, Founder respects Tech Expertise | Engineering Blog, Open Source Contribution Time |
Резюме (TL;DR для финального разговора)
> «Структура Solo FE + BE + DevOps + Design + Founder(PM) — это высокая автономность и высокая ответственность. Идеально для меня, если: > > 1. Есть DevOps — я фокусируюсь на продукте, а не на YAML/Кластерах. > 2. Founder уважает техническую экспертизу — я веду FE-архитектуру, стек, качество, найм. > 3. Домен (HR Tech) интересен: сложные формы, реал-тайм, поиск, календари, аналитика — есть где применить Senior-скиллы (RHF+Zod, TanStack Query/Virtual, WebSockets, Next.js SEO/SSR, a11y). > 4. Понятен план по Bus Factor — бюджет на найм второго FE или внешний код-ревью на старте. > > Готов двигаться к офферу, если совпадают ожидания по компенсации (Base + Equity), рабочий режим (Remote/Async-first) и культура доверия к инженерным решениям».
Вопрос 24. Как организованы внутренние процессы: планирование, трекинг задач, коммуникация?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:19:13"/>
Ответ собеседника: правильный. Уточнил детали: используют Trello для задач, Telegram для общения, ежедневные дейлики в 11:00 МСК, минимальная бюрократия, нет тайм-трекеров, оценки по честному слову, доверительные отношения.
Правильный ответ:
Описанная схема (Trello + Telegram + Daily 11:00 MSK + No Time Trackers + Trust-Based) — это классический «Startup Mode» / «Low Process Maturity». Для команды 4–5 человек (FE, BE, DevOps, Design, Founder/PM) это нормально и даже эффективно на старте, но как Senior/Tech Lead я вижу здесь зоны роста и риски, которые нужно адресовать проактивно, чтобы не утонуть при масштабировании.
1. Диагностика текущего состояния (As-Is Analysis)
| Процесс | Инструмент / Практика | Оценка (1-5) | Комментарий Senior |
|---|---|---|---|
| Task Tracking | Trello | 3/5 | Хорош для Kanban/визуализации. Слаб: нет иерархии (Epic -> Story -> Subtask), слабая аналитика (CFD, Velocity, Burndown), нет нативной связи с Git (PR -> Card). |
| Communication | Telegram | 2/5 | Риск #1. Потеря контекста, решение вопросов в личках/голосовых без фиксации в задаче, сложно искать историю решений (Search UX слабее Slack/Discord). Нет тредов (до недавнего времени). |
| Planning / Sync | Daily 11:00 MSK | 3/5 | Хорошо для синхрона. Риск: превращается в отчет «что делал/буду делать» вместо блокеров. Нет еженедельного планирования / ретро / демо. |
| Estimation | «По честному слову» (Gut feeling) | 2/5 | Риск #2. Нет калибровки (Planning Poker), нет исторической вэлосити. Оценки часто занижены (оптимизм) или завышены (страх). Нет предиктивности релизов. |
| Bureaucracy / Control | Минимум / Нет тайм-трекеров | 5/5 (для текущего размера) | Культура доверия — суперсила маленькой команды. Не убивать её процессами раньше времени. |
| Documentation / ADR | Не упомянуто (вероятно, нет) | 1/5 | Риск #3. Bus Factor = 1. Решения в голове у фаундера/лидов. Нет RFC/ADR для архитектурных выборов. |
2. Стратегия эволюции процессов (To-Be Roadmap)
Не нужно внедрять Jira/Scrum «вчера». Эволюция должна быть по требованию боли (Pain-Driven).
Этап 1: «Укрепление фундамента» (Сейчас / Первые 30 дней)
Цель: Снизить Bus Factor, сделать работу видимой, защитить фокус.
| Практика | Инструмент / Формат | Зачем |
|---|---|---|
| Async Daily (Text Standup) | Бот в Telegram / Slack (Geekbot, Standuply) или просто тред в чате | Освобождает 11:00 для глубокой работы. Текст остается в истории. Синхронный колл — только по флагу «Блокер». |
| Definition of Ready (DoR) & Done (DoD) | Чек-лист в описании доски Trello / Confluence / Notion | Защита от приема сырой задачи (нет макета, нет API спека, нет критериев приемки). |
| PR Template + Link to Trello Card | GitHub/GitLab PR Template | Обязательная трассируемость: Код -> Задача -> Требование. |
| Architecture Decision Records (ADR) | Папка /docs/adr в репо (Markdown) | Фиксация решений: «Почему TanStack Query, а не RTK Query?», «Почему Next.js, а не Vite?». |
| Weekly Demo / Sync (30 мин) | Zoom / Google Meet (в запись) | Демонстрация сделанного Founder'у/Дизайнеру. Синхронизация ожиданий. Получение фидбека рано. |
Этап 2: «Предиктивность и Качество» (Когда команда 5-7 чел / появляется 2-й FE)
Цель: Уметь обещать дедлайны и держать их.
| Практика | Инструмент / Формат | Зачем |
|---|---|---|
| Planning Poker / Affinity Estimation | Встроено в Trello (Power-Up) или PlanningPoker.com | Калибровка оценок. Выявление неясностей до старта работы. |
| Velocity / Throughput Tracking | Экспорт из Trello -> Google Sheets / Notion / Linear (импорт) | Понимание реальной пропускной способности. Планирование квартала. |
| Refinement (Grooming) Session | 1 раз в неделю / 45 мин | Разбор предстоящих задач, разбиение на мелкие (< 2 дней), уточнение требований у PM/Дизайнера. |
| Retrospective | 1 раз в 2 недели (FunRetro, Miro, Mural) | Непрерывное улучшение процессов. «Что мешает двигаться быстро?» |
| Миграция на Linear / GitHub Projects / Jira | Если Trello тесен (нет Epic, нет Roadmap view, нет Analytics) | Linear — идеальный баланс простоты и мощности для современных команд. |
Этап 3: «Масштабирование» (Команда 10+ / Несколько сквадов)
- Переход на Shape Up / Dual-Track Agile (Discovery + Delivery).
- Выделение Platform/Enabling Team (UI Kit, Infra, CI/CD).
- Внедрение OKRs для связывания фич с бизнес-метриками.
3. Конкретные риски текущего сетапа и мои действия (Mitigation)
Риск 1: Telegram как единственный хаб знаний
> Проблема: Решение в голосовом сообщении / личке -> через неделю никто не помнит, почему так сделано. Onboarding нового FE = «спроси у [Ваше Имя]». > > Мое действие: > 1. Правило: «Если решение не в Trello/ADR/PR — его не существует». > 2. Внедрю Slack / Discord (треды, поиск, интеграции с GitHub, Sentry, Vercel) или настройку Telegram Topics / Forum (если остаемся в TG) + бот-логгер важных решений. > 3. Все технические обсуждения — только в тредах задач (Trello Comments) или PR.
Риск 2: Оценки «по честному слову»
> Проблема: Founder спрашивает: «Когда будет готово?» -> Вы: «На неделе». -> Проходит 2 недели. Потеря доверия. > > Мое действие: > 1. Внедрю Range Estimates (например, «2-4 дня» или «S / M / L / XL» с привязкой к историческим данным). > 2. Буду давать конфиденциальность: «С 80% уверенностью уложусь в 3 дня, риск — интеграция с X API». > 3. Буду трекать Cycle Time (время от «In Progress» до «Done») в Trello — это лучшая метрика для прогнозов без стори-пойнтов.
Риск 3: Trello не масштабируется на Roadmap
> Проблема: Невозможно показать Founder'у дорожную карту на квартал, зависимости задач, прогресс по Эпикам. > > Мое действие: > 1. Настрою Trello Power-Ups: «Epic Cards», «Dashboard», «Calendar», «GitHub Integration». > 2. Если боли станет много — предложу Linear (бесплатно для небольших команд, невероятно быстрое, нативные Git-интеграции, Cycles, Roadmaps, Triages). Миграция за 1 час.
Риск 4: Daily в 11:00 MSK — единственная синхронизация
> Проблема: Если BE заблокирован FE или наоборот — ждем до следующего утра.
>
> Мое действие:
> 1. Введу Slack/Telegram канал #dev-sync или #blockers для мгновенного поднятия флага.
> 2. Практика «Pair Programming / Huddle» по требованию (Discord/Slack Huddle / Tuple) вместо ожидания дейли.
4. Мой комфорт в таком окружении (Senior Fit)
> «Я комфортно чувствуюсь в среде High Trust / Low Ceremony. Не мне нужен микроменеджмент или тайм-трекеры, чтобы работать эффективно. > > Что я принесу в эти процессы (без ущерба скорости): > 1. Структуру в хаос: DoR/DoD, PR Template, ADR — это не бюрократия, это автопилот, снижающий когнитивную нагрузку. > 2. Прозрачность для Founder'а: Простые дашборды (Cycle Time, Throughput, WIP) вместо «на веру». > 3. Качество связи: Перевод решений из Telegram в артефакты (Code, Docs, Comments), чтобы команда могла расти без потери контекста. > 3. Защита фокуса: Async-коммуникация по умолчанию, минимум встреч, глубокая работа как приоритет. > > Если команда вырастет до 2-х FE — я инициирую переход на Linear и введу Refinement / Retro, чтобы не потерять качество при масштабировании».
Вопрос 25. Проводится ли код-ревью, учитывая, что по одному человеку в роли?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:21:58"/>
Ответ собеседника: правильный. Код-ревью не проводятся из-за отсутствия второго фронтендера и бэкендера; фаундер объяснил, что скорость доставки приоритетнее, проект должен быстро прийти в рабочее состояние.
Правильный ответ:
Это осознанный бизнес-трейдофф (Trade-off), типичный для ранних стадий (Pre-Seed / Seed / MVP), но с точки зрения Senior Engineering это технический долг с высокой процентной ставкой, который требует немедленной митигации (смягчения последствий) и плана погашения.
Отсутствие человеческого ревью — не повод писать плохой код. Это повод автоматизировать Quality Gates до максимума и изменить личную дисциплину разработки.
1. Риски режима «No Code Review» (Почему это опасно даже для MVP)
| Риск | Последствие для бизнеса | Срок проявления |
|---|---|---|
| Bus Factor = 1 (Критический) | Если вы выбываете (болезнь, уход, форс-мажор) — команда получает «черный ящик» без документации, без понимания почему так сделано, без истории решений. Онбординг нового FE = недели археологии. | Мгновенно (в момент ухода). |
| Слепота к своим ошибкам (Blind Spots) | Логические баги, race conditions, утечки памяти, нарушения a11y, security issues (XSS, CSP), плохая архитектура (God Components, Circular Deps) попадают в прод. | Накопительно (каждый коммит). |
| Отсутствие Knowledge Sharing | Никто не учится. Вы не получаете фидбека по API дизайну, неймингу, паттернам. Рост экспертизы останавливается. | Долгосрочно (стагнация). |
| Сложность рефакторинга | Страх ломать работающее без страховки ревью/тестов ведет к «лапке» и копипасте вместо абстракций. | Через 3-6 мес. |
| Сложность передачи контекста Founder'у | Founder не может оценить качество инженерии, только скорость фич. Ожидания расходятся с реальностью. | Постоянно. |
2. Стратегия выживания: «Automated Code Review» + «Self-Review Discipline»
Раз второго пары глаз нет, CI/CD становится вашим строгим ревьюером, а вы — своим же лучшим критиком.
А. Жесткие Quality Gates в CI (Non-negotiable)
Настройте пайплайн так, чтобы мерж в main/master был невозможен без прохождения всех проверок. Это ваш «стажер-ревьюер», который не спит и не ошибается в рутине.
# .github/workflows/ci.yml (Суть конфига)
jobs:
quality-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
# 1. Статический анализ (Стиль, Best Practices, Potential Bugs)
- run: npm run lint # ESLint (strict) + Stylelint + TypeScript ESLint
# 2. Типизация (Компилятор как первый ревьюер)
- run: npm run typecheck # tsc --noEmit (strict: true, noUncheckedIndexedAccess: true)
# 3. Форматирование (Никаких споров про пробелы)
- run: npm run format:check # Prettier --check
# 4. Юнит/Интеграционные тесты (Покрытие логики)
- run: npm run test:run -- --coverage
# Пороги: lines: 80%, branches: 70%, functions: 80%
# 5. E2E тесты (Критичные пользовательские сценарии)
- run: npm run test:e2e # Playwright (Parallel, Sharded)
# 6. Сборка продакшена (Проверка на ошибки сборки, tree-shaking)
- run: npm run build
# 7. Анализ бандла (Предотвращение блота)
- run: npm run build:analyze # vite-bundle-visualizer / webpack-bundle-analyzer -> Fail if > Budget
# 8. Security / Deps Audit
- run: npm audit --audit-level=high
# или лучше: OWASP Dependency Check / Snyk / Socket.dev
Б. Self-Review Protocol (Ритуал перед пушем)
Перед созданием PR / пушем в main вы проводите Self-Review по чек-листу (выделяете 10-15 мин):
git diff main...HEAD— смотрите весь дифф в IDE (VS Code / WebStorm) или GitHub UI.- Чек-лист Self-Review:
- Single Responsibility: Компонент/функция делает одну вещь?
- Naming: Переменные/функции/компоненты говорят зачем, а не как? Нет
data,info,handler,utils. - Types: Нет
any,unknownразумно узнены, дженерики использованы там, где нужно переиспользование. - Performance: Нет лишних ре-рендеров (
React.memo,useMemo,useCallbackтам, где нужно)? Нет тяжелых вычислений в рендере? - Accessibility: Семантическая верстка,
aria-*, фокус, контраст. - Error Handling:
try/catch/Error Boundaries/TanStack Query error states/Zod validationна входах. - Security: Нет секретов в коде, пользовательский ввод санитизирован/валидирован (XSS), CSP не нарушается.
- Tests: Покрыты edge cases и business logic, не только «happy path»?
- Docs/Comments: Сложная логика прокомментирована почему (JSDoc / TSDoc), а не что.
В. Pull Requests — обязательны (даже для себя)
- Создаете PR в
main(не коммитите напрямую). - Base:
main<- Compare:feature/branch. - Description: Обязательно заполняете шаблон:
## What / WhyСсылка на Trello/Linear: [TASK-123]Что делает: ...Почему так: (Trade-offs, альтернативы)## Testing- Unit: `npm run test:run -- Feature.test.tsx`- E2E: `npm run test:e2e -- feature.spec.ts`- Manual: [Скринкаст / Ссылка на Preview Deploy (Vercel/Netlify/Cloudflare Pages)]## Checklist (Self-Review)- [ ] Lint / Typecheck / Build passed locally- [ ] Self-review done (diff checked)- [ ] No console.log / debugger- [ ] Migration / Seeds added (if DB changes)
- Мержите только когда CI зеленый. Это создает историю, контекст и точку отката.
Г. Preview Deployments (Visual Review для Founder/Designer)
- Настройте Vercel / Netlify / Cloudflare Pages / Surge.sh на автоматический деплой каждого PR.
- Founder/Designer кликают по ссылке в PR -> смотрят в браузере -> пишут комментарии в PR (Visual Feedback).
- Это частично заменяет ревью UI/UX со стороны дизайнера/PO.
3. План погашения долга (Roadmap to "Normal")
| Триггер | Действие | Срок |
|---|---|---|
| Найм 2-го FE (Junior/Middle) | Внедрение обязательного Code Review. Настройка CODEOWNERS (вы — owner src/**). Настройка Required Reviews = 1 в Branch Protection Rules. | День 1 появления коллеги. |
| Найм Senior/Lead FE | Передача архитектурного владения. Вы фокусируетесь на сложных доменах / инфраструктуре. | По мере масштабирования. |
| До найма (Прямо сейчас) | Внешний аудит (раз в квартал / перед крупным релизом). Нанимаете фриланс-эксперта (Senior/Staff) за фиксированную плату за ревью архитектуры, безопасности, перформанса ключевых модулей. Стоит дешево, спасает от катастроф. | Прямо сейчас (инициируйте сами). |
| Постоянно | ADR (Architecture Decision Records) в /docs/adr. Каждое значимое решение (State mgmt, Routing, UI Kit, Auth strategy) — файл в репе. Это ваш «ревьюер из будущего». | Начиная с вчерашнего дня. |
4. Как объяснить это Founder'у (Business Language)
> «Скорость сейчас — это не "коммитим в мастер без взгляда". Скорость — это автоматизированные ворота качества, которые не дают нам сломать прод в пятницу вечером и не заставят тратить 3 дня на отлов регрессии, которую поймал бы тест за 3 минуты.
>
> Мой подход сейчас:
> 1. CI — мой строгий ревьюер (Lint, Types, Tests, Build, Security, Bundle Size). Мёрж только по зеленым галочкам.
> 2. Self-Review + PR Template — дисциплина, создающая историю и контекст для будущей команды.
> 3. Preview Deploys — быстрая обратная связь от тебя/дизайнера без сборки локально.
> 4. ADR — документация решений, чтобы новый разработчик не спрашивал "почему так?".
>
> План: Как только появляется 2-й FE — мгновенно включаем Required Code Review (1 approver) и CODEOWNERS. Это не замедлит нас, это ускорит онбординг нового человека и снизит Bus Factor с 1 до 2».
Резюме (TL;DR)
> «Да, человеческого код-ревью сейчас нет — это осознанный риск MVP-стадии. Но качество не компрометируется. > > Я заменил ревьюера автоматизированным пайплайном (Strict CI: Lint, Typecheck, Unit/E2E Tests, Build, Bundle Audit, Security) и дисциплиной Self-Review через Pull Requests с шаблоном. > > Внедряю ADR для передачи контекста. Настроил Preview Deployments для визуального контроля Founder/Design. > > План выхода: При найме 2-го FE — мгновенно включаю Branch Protection + Required Review + CODEOWNERS. Готов провести внешний аудит архитектуры уже сейчас для снижения рисков».
Вопрос 26. Как устроено взаимодействие с бэкендом? Есть ли документация?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:22:24"/>
Ответ собеседника: правильный. Обычные REST-ручки, задокументированы в Swagger/OpenAPI. Сложные кейсы (вебсокеты) обсуждаются отдельно. Бэкендер ведет документацию по каждому сервису.
Правильный ответ:
Наличие OpenAPI (Swagger) — это отличный фундамент, но на уровне Senior/Tech Lead взаимодействие с бэкендом не заканчивается на «есть доки». Ключевы — контракт-ориентированная разработка (Contract-First), автоматизация типизации, валидация рантайма и процесс управления изменениями.
1. Текущее состояние (As-Is) и риски
| Аспект | Описание | Риск / Проблема |
|---|---|---|
| Формат | REST + JSON, OpenAPI 3.0 (Swagger UI) | Хорошо. Стандарт де-факто. |
| Вебсокеты | Отдельные обсуждения, документация может отставать | Риск #1: Нет формального контракта (AsyncAPI). Согласование в чате/звонке -> рассинхронизация. |
| Владение доками | Бэкендер ведет «по каждому сервису» | Риск #2: Фронт — пассивный потребитель. Нет CI-проверки актуальности схемы. Нет breaking change detection. |
| Генерация кода | Не упомянута (вероятно, ручные типы/обертки) | Риск #3: Дублирование типов (TS интерфейсы в фроте + схема в бэке). Ручная ошибка при изменении поля. |
2. Целевой процесс: Contract-First Development (To-Be)
Внедряю с первого дня (или мигрирую за 1-2 спринта) следующий пайплайн:
А. Источник правды — OpenAPI Spec в репозитории (или Registry)
- Спека хранится в Git (monorepo:
packages/api-contract/openapi.yamlили отдельный репоapi-specs). - Не только в Swagger UI (который генерируется из кода бэка). Нужен доступ к «сырому» yaml/json для генерации на фронте.
Б. Автоматическая генерация клиента и типов (Codegen)
Использую Orval / Hey API (openapi-ts) / openapi-typescript-codegen в CI / pre-commit / локально.
Конфиг Orval (orval.config.ts) — стандарт проекта:
export default {
hiringPlatform: {
input: 'packages/api-contract/openapi.yaml', // или URL к Swagger JSON бэка
output: {
target: 'shared/api/generated/endpoints.ts',
client: 'tanstack-query', // Генерирует хуки useQuery/useMutation сразу!
override: {
mutator: {
path: 'shared/api/http.ts',
name: 'customFetcher', // Наш Ky/Axios инстанс с интерцепторами
},
query: {
useQuery: true,
useMutation: true,
// useSuspenseQuery: true, // если RSC
},
},
// Валидация рантайма — критично для безопасности типов
schemas: 'zod', // или 'valibot' / 'arktype'
// Чистые типы для ручного использования
// model: true,
},
},
};
Результат: npm run generate:api -> Готовые типизированные хуки (useGetVacancies, useCreateCandidateMutation), Zod-схемы для валидации ответов, TS типы DTO. Никаких ручных интерфейсов.
В. Валидация рантайма (Runtime Validation) — «Trust, but verify»
Сгенерированные Zod/Valibot схемы используются в API слое для парсинга входящих данных.
// shared/api/http.ts (Ky instance)
import { z } from 'zod';
// Пример обертки для безопасного парсинга
export const safeParse = <T>(schema: z.ZodSchema<T>, data: unknown): T => {
const result = schema.safeParse(data);
if (!result.success) {
// Логируем в Sentry/LogRocket с контекстом
captureException(new ApiValidationError(result.error, schema));
throw result.error; // или кастомная ошибка
}
return result.data;
};
// В сгенерированном endpoints.ts (или кастомной обертке) это вызывается автоматически
Г. Contract Testing & Breaking Change Detection (CI Gate)
В CI пайплайне фронтенда (и бэкенда) добавляю проверку совместимости.
Инструменты: openapi-changes / swagger-cli / Spectral / Optic / Postman CLI.
# .github/workflows/api-contract-check.yml
jobs:
contract-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Скачиваем актуальную спеку бэка (из артефакта бэка / registry / URL)
- name: Fetch Backend Spec
run: curl -s ${{ secrets.BACKEND_SWAGGER_URL }} > backend-spec.yaml
- name: Check Breaking Changes
# Сравниваем с версией, закоммиченной во фронте (или предыдущей версией бэка)
run: |
npx @apidevtools/openapi-changes \
--old shared/api/contract/openapi.yaml \
--new backend-spec.yaml \
--breaking \
--output markdown > breaking-changes.md
- name: Fail on Breaking Changes
if: always()
run: |
if [ -s breaking-changes.md ]; then
echo "❌ BREAKING CHANGES DETECTED:"
cat breaking-changes.md
exit 1
fi
Правило: Мерж в main бэка/фронта блокируется, если есть breaking changes без миграции (major version bump / coordinated deploy).
Д. Вебсокеты: AsyncAPI + Codegen
Для WS не использую «договорились в телеге».
- Бэкенд описывает WS события в AsyncAPI (YAML).
- Фронт генерирует типы/клиент:
@asyncapi/generator-> TypeScript типы событий, типизированные слушатели/эмиттеры. - Единый источник правды для
onMessage,emitEventpayloads.
3. Процесс взаимодействия (Workflow)
| Этап | Действие | Ответственный | Артефакт |
|---|---|---|---|
| 1. Design | Обсуждение новой фичи / изменения API. Создание RFC / ADR в репе. | FE Lead + BE Lead + PM | docs/adr/005-new-candidate-flow.md |
| 2. Contract Draft | Бэкенд пишет/обновляет OpenAPI/AsyncAPI спеку (Design First) или обновляет код с аннотациями (Code First -> Spec Gen). | Backend | openapi.yaml (Draft branch) |
| 3. Review | Фронт ревьюит спеку (PR в репу контрактов / комменты в Swagger Hub / Optic). Проверяем: удобство типов, пагинация, фильтрация, ошибки, идемпотентность. | Frontend (Вы) | Approved PR / Comments |
| 4. Mock & Dev | Фронт генерирует моки (MSW) из спеки -> параллельная разработка FE/BE. | Frontend | mocks/handlers.ts (MSW) |
| 5. Implementation | Бэкенд реализует. Фронт использует сгенерированные хуки. | Both | Code |
| 6. Contract Test | CI прогоняет openapi-changes / pact (если consumer-driven). | CI/CD | Pass/Fail |
| 7. Deploy | Координированный деплой (Feature Flags на фронте для плавного переключения). | DevOps + FE/BE | Release |
4. Чек-лист Code Review спецификации (Что я смотрю в OpenAPI PR)
- Версионирование:
/api/v1/...в пути. Нет breaking changes вv1(только additive: новые опциональные поля, новые эндпоинты). - Стандартизация ошибок: Единый формат
ProblemDetails(RFC 7807) /{ code, message, details }для всех 4xx/5xx.components.schemas.ErrorResponse. - Пагинация: Только курсорная (
cursor/limit) или offset-based (page/size+total) — единообразно везде. - Фильтрация/Сортировка: Стандартные query params (
?filter[status]=active&sort=-createdAt). - Идемпотентность:
Idempotency-Keyзаголовок дляPOST/PATCH/DELETE(критично для платежей, отправки писем). - Безопасность:
securitySchemes(BearerAuth, ApiKey) описаны и применены ко всем защищенным эндпоинтам. - Примеры:
examplesв схемах для Swagger UI и генерации моков. - Deprecation: Поле
deprecated: true+x-sunset-dateдля удаляемых полей/эндпоинтов.
Резюме (TL;DR)
> «Сейчас: REST + Swagger + устные договоренности по WS. Это работает, но хрупко.
>
> Мой стандарт — Contract-First с автоматизацией:
> 1. OpenAPI/AsyncAPI в Git — Single Source of Truth.
> 2. Orval / Hey API -> Генерирую TanStack Query хуки + Zod/Valibot схемы + TS типы. Никаких ручных DTO.
> 3. Runtime Validation (Zod.parse) на границе приложения — защита от «бэкенд прислал строку вместо числа».
> 4. CI Gate: openapi-changes — блокируем деплой при breaking changes без согласования.
> 5. MSW Mocks из той же спеки — параллельная разработка FE/BE.
> 6. AsyncAPI для WebSockets — типизированные события вместо any.
>
> Это переводит взаимодействие из режима «договорились в телеге» в режим «сломал контракт — CI упадет, не зальешь в прод». Готов настроить этот пайплайн за 1-2 дня».
Вопрос 27. Планируется ли внедрение веб-сокетов?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:23:00"/>
Ответ собеседника: правильный. Возможно через пару месяцев для чатов между рекрутером и соискателем; пока только черновики.
Правильный ответ:
Чаты рекрутера и соискателя — это классический кейс для WebSockets (двусторонний обмен в реальном времени, низкая задержка, наличие состояния подключения). «Черновики» на стадии проектирования — правильный момент, чтобы заложить архитектуру, которая выдержит нагрузку и не станет болевой точкой через год.
Ниже — Senior/Tech Lead чек-лист готовности к внедрению WebSockets в текущем стеке (Solo FE, BE, DevOps, Founder).
1. Выбор протокола и инфраструктуры (Transport & Infra)
| Вариант | Вердикт для вашего кейса | Обоснование |
|---|---|---|
| Raw WebSockets (ws / socket.io на своем Node/Go) | ❌ Не рекомендую | Вам придется самим писать: реконнекты, бэкпрешшр, горизонтальное масштабирование (Redis PubSub / Sticky Sessions), авторизацию при хендшейке, мониторинг соединений. DevOps у вас один — не нагружайте его поддержкой кастомного WS-кластера. |
| Socket.io (Self-hosted) | ⚠️ Только если нужен Fallback (HTTP Long-polling) | Добавляет оверхед (~30% к трафику), требует Sticky Sessions / Redis Adapter для скейлинга. Для современного HR-портфолио (PWA, мобильные браузеры) нативный WS достаточно. |
| Managed Service: Pusher / Ably / Centrifugo / Socket.io Cloud | ✅ Рекомендую (Best Choice) | DevOps-free скейлинг. Платите за MAU/соединения. Дают: Presence (кто онлайн), History (последние сообщения при реконнекте), Webhooks (события в BE), Auth Tokens (подпись на бэке). Centrifugo (Open Source, Go) — лучший баланс: ставите в K8s (DevOps делает это за 1 час), нет vendor lock-in, протокол бинарный/JSON, есть Presence/History/RPC. |
| GraphQL Subscriptions (over WS) | ❌ Overhead | Если у вас REST — не вводите GraphQL только ради чатов. |
Мой выбор: Centrifugo (Self-hosted) или Ably/Pusher (Managed).
- Если DevOps готов держать 1-2 поды Centrifugo в K8s + Redis — идеально, дешево, полный контроль.
- Если DevOps загружен — Ably (Generous Free Tier, отличный DX, Webhooks в BE).
2. Архитектура интеграции (Backend ↔ Centrifugo/Ably ↔ Frontend)
graph LR
FE[Frontend\nReact] <-- WS (WebSocket) --> Centrifugo[Centrifugo / Ably]
BE[Backend\nGo/Node] <-- HTTP API / gRPC --> Centrifugo
BE <-- Webhooks --> BE_Logic[Business Logic\nChat Service]
DB[(PostgreSQL\nMessages)] <-- SQL --> BE_Logic
Поток сообщения (Happy Path):
- FE коннектится к Centrifugo с JWT (Connection Token), подписанным Backend'ом (claims:
user_id,role,channels: ["chat:123"]). - FE подписывается на канал
chat:{vacancy_id}:{candidate_id}(Private Channel). - Recruiter пишет сообщение в UI -> FE вызывает BE REST API
POST /api/chats/{id}/messages(Idempotency-Key!). - BE сохраняет в БД, проверяет права, публикует событие в Centrifugo (HTTP API:
POST /api/publish-> channelchat:..., data:{ message: ... }). - Centrifugo раздает сообщение всем подписчикам (Recruiter + Candidate) за < 10ms.
- FE получает событие -> Оптимистично обновляет UI (или ждет эхо от сервера для подтверждения ID).
Почему публикация через BE, а не Client-to-Client?
- Безопасность/Валидация: BE — единственный источник правды. Нельзя доверять клиенту формировать итоговый объект сообщения (ID, timestamp, системные поля).
- Надежность: Если BE упал — сообщение не отправится (правильно). Если клиент опубликует напрямую — сообщение уйдет, но в БД его не будет (рассинхрон).
- Webhooks: Centrifugo -> BE Webhook (
on_subscribe,on_publish,on_disconnect) позволяет BE трекать онлайн-статус, читать квитанции (read receipts), триггерить Push-уведомления (FCM/APNs) для офлайн пользователей.
3. Frontend Implementation (React + TanStack Query + WS)
А. Типизация (AsyncAPI / Zod)
// shared/api/ws/schemas.ts
import { z } from 'zod';
export const WSMessageSchema = z.object({
event: z.literal('message'),
data: z.object({
id: z.string().uuid(),
chatId: z.string().uuid(),
senderId: z.string().uuid(),
content: z.string().max(5000),
type: z.enum(['text', 'system', 'file']),
createdAt: z.string().datetime(),
// Оптимистичный ID для сопоставления
tempId: z.string().uuid().optional(),
}),
});
export type WSMessage = z.infer<typeof WSMessageSchema>;
Б. Хук управления соединением (Singleton Pattern)
// shared/lib/ws/useCentrifugo.ts
import { useEffect, useRef, useCallback } from 'react';
import { Centrifuge } from 'centrifuge'; // или ably-js
import { useAuthStore } from 'entities/user/model/store';
import { WSMessageSchema } from 'shared/api/ws/schemas';
export const useChatWS = (chatId: string, onMessage: (msg: WSMessage) => void) => {
const token = useAuthStore(s => s.wsToken); // JWT для подключения к Centrifugo
const centrifugeRef = useRef<Centrifuge | null>(null);
const subscriptionRef = useRef<Centrifuge.Subscription | null>(null);
useEffect(() => {
if (!token || !chatId) return;
// 1. Инициализация клиента (Singleton на сессию)
if (!centrifugeRef.current) {
const cf = new Centrifugo(process.env.NEXT_PUBLIC_CENTRIFUGO_WS_URL!, {
token: token, // Функция или строка, возвращающая JWT
// Автореконнект с экспоненциальным бэкоффом встроен
debug: process.env.NODE_ENV === 'development',
});
cf.connect();
centrifugeRef.current = cf;
}
const cf = centrifugeRef.current;
// 2. Подписка на приватный канал чата
const sub = cf.newSubscription(`chat:${chatId}`);
sub.on('publication', (ctx) => {
// 3. Валидация рантайма (защита от грязных данных)
const result = WSMessageSchema.safeParse(ctx.data);
if (result.success) {
onMessage(result.data);
} else {
console.error('Invalid WS payload', result.error, ctx.data);
}
});
sub.on('subscribed', () => console.log(`Subscribed to chat:${chatId}`));
sub.on('unsubscribed', (ctx) => console.warn(`Unsubscribed: ${ctx.reason}`));
sub.subscribe();
subscriptionRef.current = sub;
// Cleanup
return () => {
sub.unsubscribe();
// Не отключаем centrifuge глобально, если есть другие открытые чаты
// Логика рефкаунтинга подписок вынесена в отдельный менеджер (см. ниже)
};
}, [chatId, token, onMessage]);
};
В. Интеграция с TanStack Query (Optimistic Updates + Sync)
// features/chat/ui/ChatMessageList.tsx
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useChatWS } from 'shared/lib/ws/useChatWS';
import { api } from 'shared/api/generated';
export const ChatMessageList = ({ chatId }: { chatId: string }) => {
const qc = useQueryClient();
const queryKey = ['chats', chatId, 'messages'];
// 1. Загрузка истории (REST)
const { data: messages = [] } = useQuery({
queryKey,
queryFn: () => api.chats.getMessages(chatId),
});
// 2. Отправка сообщения (REST + Optimistic)
const sendMutation = useMutation({
mutationFn: (content: string) => api.chats.sendMessage(chatId, { content, tempId: crypto.randomUUID() }),
onMutate: async (newMsg) => {
await qc.cancelQueries({ queryKey });
const prev = qc.getQueryData(queryKey);
// Оптимистичное добавление
qc.setQueryData(queryKey, (old: any[]) => [...old, { ...newMsg, status: 'sending', tempId: newMsg.tempId }]);
return { prev };
},
onError: (err, vars, ctx) => qc.setQueryData(queryKey, ctx?.prev),
onSuccess: (serverMsg, vars) => {
// Заменяем оптимистичный (tempId) на серверный (id)
qc.setQueryData(queryKey, (old: any[]) =>
old.map(m => m.tempId === vars.tempId ? { ...serverMsg, status: 'sent' } : m)
);
},
});
// 3. WebSocket слушатель входящих / эха своих сообщений
useChatWS(chatId, useCallback((wsMsg) => {
// Если это наше эхо (по tempId) — обновляем статус
// Если чужое — добавляем в список
qc.setQueryData(queryKey, (old: any[]) => {
if (old.some(m => m.id === wsMsg.data.id)) return old; // Дедуп
return [...old, { ...wsMsg.data, status: 'received' }];
});
}, [chatId, qc]));
// UI Render...
return (
<VirtualList data={messages} renderItem={MessageBubble} />
);
};
4. Критичные нефункциональные требования (NFR) — обсудить с BE/DevOps сейчас
| Требование | Решение | Кто отвечает |
|---|---|---|
| История сообщений при реконнекте | Centrifugo History (включить в конфиге канала, хранить последние 100-500 в Redis) + BE REST GET /messages?before=... для ленивой подгрузки. | BE (настройка Centrifugo) + FE (UI Scroll Restoration). |
| Push-уведомления (Mobile/Background) | Centrifugo Webhook on_disconnect / on_publish -> BE -> Firebase (FCM) / APNs. Токены девайсов хранятся в BE. | BE (Push Service) + Mobile/FE (Token registration). |
| Прочтение / Статусы доставки | Client-side ACK: FE получает msg -> шлет ack в канал / BE REST PATCH /messages/{id}/read. BE обновляет БД -> публишит read_receipt в канал. | FE + BE (Protocol Design). |
| Файлы / Медиа | Только через BE: FE -> Presigned URL (S3) -> Upload -> BE получает вебхук/проверяет -> сохраняет метаданные в БД -> публишит сообщение типа file с CDN URL. Никаких бинарников в WS. | BE (Storage) + FE (Uppy/Dropzone). |
| Масштабирование | Centrifugo: Redis Engine (PubSub) + Multiple Nodes. Sticky Sessions не нужны (Centrifugo умеет проксировать подписки между нодами через Redis). | DevOps (K8s Deployment + Redis Cluster). |
| Мониторинг | Метрики: active_connections, messages_per_sec, publish_latency_p99, connection_errors. Дашборд в Grafana. Алерт: connections > threshold / latency > 500ms. | DevOps (Prometheus/Grafana) + BE (Metrics Export). |
5. План внедрения (Roadmap) — «Черновики» -> Прод
| Этап | Срок | Деливерабл |
|---|---|---|
| 1. Spike / PoC (1 неделя) | Сейчас | FE: Подключение к Centrifugo (Dev), отправка/получение сообщения в консоли. BE: Публикация через HTTP API. DevOps: Поднять Centrifugo + Redis в Staging. |
| 2. Core Chat MVP (2 недели) | Спринт 1 | FE: Список чатов, виртуализированный список сообщений, оптимистичный UI, реконнект. BE: REST CRUD сообщений, Webhooks на Push, сохранение в БД. |
| 3. Hardening (1 неделя) | Спринт 2 | История (Centrifugo History), Прочитано/Доставлено, Обработка ошибок сети (Offline Queue в FE -> IndexedDB), Файлы (Presigned URL). |
| 4. Polish & Scale (Постоянно) | Пост-MVP | Push-уведомления (FCM/APNs), Поиск по сообщениям (Elasticsearch/Meilisearch на BE), Боты/Автоответчики, Аналитика (Time-to-reply). |
Резюме (TL;DR для обсуждения с Founder/Team)
> «Вебсокеты для чатов — да, обязательно, но не пишем свой WS-сервер. > > Стек: Centrifugo (Self-hosted на Go + Redis) или Ably (Managed). Это снимает головную боль со скейлинга, реконнектов, Presence и History. > > Архитектура: FE подписывается на приватные каналы по JWT. Отправка — только через REST API на BE (валидация, сохранение, права). BE публикует в Centrifugo. FE получает -> валидирует Zod -> апдейтит TanStack Query кэш. > > Критично сделать на этапе черновиков: > 1. Согласовать AsyncAPI / Zod-схемы событий (вход/выход). > 2. Запланировать DevOps задачу на поднятие Centrifugo/Redis в K8s (или выбор Managed). > 3. Заложить в BE Webhooks для Push-уведомлений офлайн-пользователям. > 4. На FE — Singleton WS Manager + интеграция с TanStack Query (Optimistic UI). > > Готов взять за 2-3 недели от «черновиков» до работающего чата в Staging, если DevOps даст инфраструктуру за 2 дня».
Вопрос 28. Какой фидбек от технического собеседования? Интересен ли проект? Какие вопросы?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:24:50"/>
Ответ собеседника: правильный. Техническое собеседование прошло хорошо, несоответствий нет. Проект интересен. Главный вопрос — сколько времени дадут на онбординг, учитывая, что фронтендер один и спрашивать по архитектуре не у кого.
Правильный ответ:
Это зрелый, стратегический вопрос, который сразу позиционирует вас как Senior/Tech Lead, а не как исполнителя. Вы не просто спрашиваете «когда начну», а оцениваете риски входа (Ramp-up Risk) и стоимость ошибки в условиях Bus Factor = 1.
Ниже — как правильно расставить акценты в диалоге с Founder'ом/CTO, чтобы получить честный ответ и защитить себя от выгорания в первый месяц.
1. Как правильно сформулировать запрос (Negotiation Frame)
> «Проект технически интересен: стек современный (React/TS/RTK Query/MUI), есть DevOps, есть дизайнер, домен (HR Tech) с понятными сложностями (формы, реал-тайм, поиск). Я вижу, как могу принести ценность. > > Мой вопрос про онбординг не про «чтобы посидеть в тишине», а про управление рисками. > Учитывая, что я единственный FE и Code Review нет, цена моей архитектурной ошибки на старте — недели рефакторинга или даунтайм. > > Мне нужно согласовать три параметра онбординга: > 1. Время на «Read-Only» режим (1-2 недели): Аудит кода, инфраструктуры, CI/CD, контрактов API, запуск локально, изучение домена. Без задач в спринт. > 2. Внешний технический спарринг-партнер (External Code Review): Бюджет на 4-8 часов в неделю Senior/Staff FE (консультант/аутстафф) на первые 1-2 месяца для ревью моих PR/ADR до найма второго FE. > 3. План найма второго FE (Headcount): Есть ли открытая вакансия, бюджет, таймлайн (Target: 1-2 месяца)?»
2. Чек-лист онбординга (Ваш «Definition of Done» для входа в работу)
Предложите Founder'у подписать (в виде тикета в Trello/Linear) этот чек-лист. Если он согласен — это зеленый флаг. Если «нет времени» — красный флаг.
| Этап | Активность | Деливерабл | Срок |
|---|---|---|---|
| Неделя 0 (Pre-start / Offer Accepted) | Доступы: GitHub/GitLab, Cloud (AWS/GCP/Vercel), Sentry, Datadog/LogRocket, Figma (DevMode), Trello/Linear, Slack/Telegram, VPN. | Чек-лист доступов пройден. | День 1 |
| Неделя 1: Discovery & Audit (Read-Only) | 1. Локальный запуск (DevBox/Docker Compose) — Must work in 1 hour.<br>2. Аудит репы: Архитектура (FSD/Modules), Зависимости, Bundle Size, TS Config, ESLint/Prettier.<br>3. Аудит CI/CD: Пайплайны, Quality Gates, Preview Deployments, Secrets Management.<br>4. Аудит API: OpenAPI Spec актуальность, генерация типов (Orval/Hey API), MSW Mocks.<br>5. Аудит Инфраструктуры: K8s/Serverless, Environments (Dev/Stage/Prod), DB Migrations, Monitoring/Alerting.<br>6. Аудит Процессов: Trello Board Structure, Branching Strategy, Release Process, On-call Schedule.<br>7. Встречи 1-on-1: BE Lead (API, WS планы), DevOps (Infra, Deploy), Designer (Design System, Figma Tokens), Founder (Roadmap, Priorities). | Документ: ONBOARDING_AUDIT.md<br>— Список Critical / Major / Minor findings.<br>— Риски (Security, Performance, Bus Factor).<br>— Предложения Quick Wins (настройка линтеров, генерация API, Feature Flags). | Конец Недели 1 |
| Неделя 2: Quick Wins & Foundation | 1. Внедрение Strict CI Gates (Typecheck, Lint, Unit/E2E Tests, Bundle Budget).<br>2. Настройка Orval/Hey API генерации + Zod Runtime Validation.<br>3. Настройка MSW для локальной разработки и CI.<br>4. Внедрение PR Template + Self-Review Checklist + ADR Template.<br>4. Настройка Preview Deployments (Vercel/Netlify/Cloudflare) на каждый PR.<br>5. Написание ARCHITECTURE.md (C4 Context/Container) + RUNBOOK.md (Deploy, Rollback, Incidents). | Зеленый CI на main.<br>Автоматизация рутины.<br>Документация для Bus Factor 2. | Конец Недели 2 |
| Неделя 3-4: First Feature & External Review | 1. Берете Small/Medium фичу (не рефакторинг ядра!).<br>2. Пишете RFC/ADR для решения.<br>3. Реализуете через PR -> External Code Review (консультант).<br>4. Деплой через Feature Flag. | Первый Production Deploy под новым процессом.<br>Обратная связь от консультанта. | Конец Месяца 1 |
3. Вопрос про «Внешнего ревьюера» — критически важен
Поскольку внутреннего ревьюера нет, вы обязаны заложить в оффер/бюджет External Code Review.
Аргументация для Founder'а: > «Стоимость часа консультанта (Senior/Staff FE) — ~1600-2400/мес. > Стоимость бага в проде, утечки данных, сломанной архитектуры или месяца простоя при уходе — на порядки выше. > Это не расход, это страховка Bus Factor = 1 до найма второго инженера. > Я могу помочь найти такого человека в своей сети (без рекрутинговых сборов).»
Альтернатива (если бюджета точно нет):
- Founder/CTO (если технический) делает Architectural Review 1 раз в неделю (30 мин) по ADR/PR.
- Внедряете Pair Programming с BE/DevOps на критичных интеграциях (API, WS, Deploy).
4. Остальные вопросы, которые стоит задать сейчас (Closing Questions)
Помимо онбординга, на финальной стадии у Senior/Tech Lead должны быть вопросы, закрывающие неопределенность:
| Тема | Вопрос | Зачем |
|---|---|---|
| Equity / Compensation | «Как выглядит пакет: Base + Equity (Options/RSU)? Вестинг (4 года, 1 год cliff)? Есть ли Refresh Grants? Как определяется Strike Price (409A)?» | Понимание реальной стоимости оффера. |
| Work-Life Balance / On-call | «Как организован On-call? Есть ли ротация (сейчас я один FE)? Компенсация за ночные инциденты? Ожидания по доступности в выходные?» | Защита от выгорания. Solo FE + On-call 24/7 = высокий риск. |
| Visa / Relocation / Legal | «Какая поддержка по релокации/визе (если актуально)? B2B контракт / Нанимательство (Employment Contract)? IP Assignment Agreement условия?» | Юридическая безопасность. |
| Tech Debt Budget | «Готовы ли зафиксировать 20% времени (или 1 день в спринте) на Tech Debt / Upgrades / Refactoring с первого спринта?» | Гарантия качества кода. |
| Performance Review | «Как проходят Performance Review? Кто оценивает (Founder / CTO / Peer)? Критерии: Delivery / Quality / Leadership / Innovation?» | Карьерная прозрачность. |
| Decision Making | «Как принимаются технические решения спорных моментов? У меня есть право вето на архитектуру FE?» | Автономность. |
Резюме (TL;DR для финального разговора)
> «Собеседование прошло по существу, стек и команда нравятся. Готов двигаться дальше. > > Мой ключевой вопрос — Онбординг. > Я прошу 2 недели «Read-Only» аудита без задач в спринт + бюджет на External Code Review (4-8h/week) на период до найма второго FE. > > Это стандартная практика для входа в Solo FE роль с нулевым Bus Factor. Это защищает бизнес от рисков «одиночки» и позволяет мне сразу навести порядок в CI, типах, деплое и документации, чтобы дальше двигаться с максимальной скоростью и качеством. > > Если мы синхронизируемся по этим пунктам — я готов подписывать оффер и начинать в ближайшие 2 недели».
Вопрос 29. Почему ушел предыдущий фронтенд-разработчик?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:26:12"/>
Ответ собеседника: правильный. Ему предложили в 2 раза больше зарплаты (X2), стартап не смог перебить оффер.
Правильный ответ:
Это стандартная ситуация для рынка (Market Reality), а не «красный флаг» компании. Уход за X2 зарплаты — это рациональное решение инженера на рынке, где спрос превышает предложение, а стартапы на ранних стадиях (Pre-Seed/Seed) редко могут конкурировать с BigTech / Late-Stage / Crypto / Fintech по кэшу.
Однако, как Senior/Tech Lead, я читаю за этим ответом важные сигналы для своих переговоров и оценки рисков.
1. Что это значит для бизнеса (Business Context)
| Фактор | Интерпретация |
|---|---|
| Валидация скиллов | Предыдущий разработчик был сильным (Middle+/Senior), если его забирают за X2. Значит, кодовая база, скорее всего, не «мусор», а рабочий продукт. |
| Бюджетная реальность | Компания не может/не хочет платить топовый маркет (Top of Market). Это стартап с ограниченным Runway или консервативной компенсационной политикой. |
| Retention Strategy | У компании нет инструментов удержания (Equity не воспринимается как замена кэшу, нет роста, нет интересных вызовов, нет WLB). |
| Bus Factor = 1 (Сейчас) | Уход одного человека обнуляет FE-компетенцию. Это операционный риск №1, который я прихожу закрывать. |
2. Вопросы, которые я задаю Founder'у / CTO (Due Diligence)
Чтобы не стать «следующим, уйдущем за X2», мне нужно понять: как компания реагирует на потерю ключевых людей?
| Вопрос | Зачем спрашиваю | Хороший сигнал (Green Flag) | Плохой сигнал (Red Flag) |
|---|---|---|---|
| «Как вы пересмотрели компенсационную политику после этого ухода?» | Понять, индексировали ли зарплаты остальным / новому. | «Да, пересмотрели бэнды, выделили бюджет на маркетные офферы + Equity». | «Не можем, у нас строгие бэнды / инвесторы не дают / Equity компенсирует». |
| «Есть ли Retention Bonus / Long-term Incentive Plan (LTIP) для ключевых инженеров?» | Проверка долгосрочной мотивации. | «Да, есть Refresh Grants / Performance Bonus / KPI-based Equity». | «Только базовая зарплата + стандартный вестинг опционов». |
| «Как вы планируете нанять замену и избежать одиночества нового FE?» | План найма и онбординга. | «Открыта вакансия, бюджет утвержден, ищем Middle/Senior, есть бюджет на аутстафф на переходный период». | «Пока ищем, бюджет ограничен, надеемся, что ты справишься один надолго». |
| «Что сделал ушедший разработчик за последние 2 недели? (Knowledge Transfer)» | Оценка состояния документации/передачи. | «Оставил ADR, обновил README, провел 2 сессии передачи с BE/DevOps, закрыл критичные PR». | «Просто ушел / передавал в Telegram голосовыми / ничего не задокументировал». |
3. Как я использую эту информацию в своих переговорах (Negotiation Leverage)
Зная, что предыдущий ушел за X2, я строю свою позицию:
А. Base Salary (Оклад)
> «Я понимаю бюджетные ограничения стартапа. Но я вижу, что маркет для моего уровня (Senior/Lead, Fullstack/Architecture, Solo Ownership) — это [Моя Цена]. > Если вы не можете дать Top-of-Market Base, давайте обсудим структуру, где Base = Market Median (P50-P75), а разница до Top-of-Market закрывается Equity / Performance Bonus / Sign-on Bonus.»
Б. Equity (Доля / Опционы) — Компенсация риска «Solo FE»
> «Поскольку я вхожу в роль Bus Factor = 1 (единственный FE), мой риск выше, чем у BE/DevOps/Design. > Я ожидаю Equity Grant выше маркета для моей роли (например, 0.5% - 1.5% vs стандартные 0.1-0.5% для Senior IC) или Accelerated Vesting (например, 1 год клифф -> месячный вестинг, или Double Trigger Acceleration на Exit).»
В. Sign-on Bonus / Relocation / Education Budget
> «Если Base жестко зафиксирован ниже маркета, прошу Sign-on Bonus (3-6 месяцев оклада) на покрытие риска перехода и покупку оборудования/обучения.»
Г. Четкий план найма второго FE (Headcount Plan)
> «Мое условие комфортной работы: утвержденная вакансия 2-го FE (Middle/Senior) с бюджетом и таймлайном найма 2-3 месяца. > Я не готов быть вечным Solo FE. Моя цель — построить архитектуру и процессы, онбордить коллегу, стать Tech Lead FE команды.»
4. Риски для меня (Personal Risk Assessment)
| Риск | Митигация (мои действия) |
|---|---|
| Burnout (Solo + On-call + Deadlines) | Жесткие границы: WIP Limits, Async-first, нет ночных деплоев без Feature Flags, External Code Review (оплачиваемый компанией). |
| Salary Stagnation | В контракте: Salary Review каждые 6 месяцев (не 1 год), привязка к маркету (P75) + Inflation Adjustment. |
| Equity Illiquidity | Понимаю, что опционы = лотерейный билет. Не считаю их частью «жизненных денег». Фокус на Base + Cash Bonus. |
| Technical Debt Inheritance | Первые 2 недели — Аудит + Quick Wins (CI, Types, Tests, Docs). Если кодовая база катастрофа — эскалирую к Founder'у с планом рефакторинга или ухожу в Probezeit. |
Резюме (TL;DR для разговора с Founder'ом)
> «Понимаю причину ухода — маркет диктует свои правила. Это сигнал, что предыдущий инженер был силен, но и что компания уязвима перед конкуренцией за таланты. > > Чтобы я не ушел по той же причине через год, мне важны: > 1. Честный Base (P75 Market) + Значимый Equity Grant (компенсация риска Solo FE / Bus Factor 1). > 2. План найма 2-го FE (бюджет, таймлайн 2-3 мес.) — я строю команду, а не работаю один навсегда. > 3. Salary Review каждые 6 мес. + Performance Bonus — защита от инфляции и маркетного дрейфа. > 4. Бюджет на External Code Review на период онбординга/найма — безопасность кода без бутылочного горлышка. > > Если мы закрываем эти пункты — я готов закрывать риск Bus Factor = 1 и выводить продукт на новый уровень».
Вопрос 30. Какие варианты оформления рассматриваются: ТК РФ или ГПХ?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:26:37"/>
Ответ собеседника: правильный. Предлагают оба варианта. Кандидат склоняется к ГПХ на старте (2-3 месяца, минимум 11 месяцев по договору), чтобы снизить риски для сторон, потом возможно ТК РФ.
Правильный ответ:
Выбор между ТК РФ (Трудовой договор) и ГПХ (Гражданско-правовой договор / Самостоятельность / ИП) — это не просто административная формальность, а стратегическое финансовое и правовое решение, влияющее на налоговую нагрузку, социальные гарантии, риски проверок (ФНС/Трудовая инспекция) и долгосрочное планирование.
Для Senior/Tech Lead уровня дохода (обычно > 300-500к руб./мес. на руки) разница в нетто и рисках колоссальна.
1. Сравнительная таблица (Senior View)
| Параметр | ТК РФ (Трудовой договор) | ГПХ / ИП (Услуги / Подряд) | ГПХ / Физлицо (Самозанятость / НПД) |
|---|---|---|---|
| Налоговая нагрузка (Работодатель) | ~30-43% сверх Gross (Взносы: Пенс 22%, ФОМС 5.1%, ФСС 2.9%, НС 0.6-8.5%) | 0% (если ИП/Самозанятость) или 30% (если физлицо без статуса) | 0% (платит исполнитель: 6% физ / 4% юр) |
| Налоговая нагрузка (Исполнитель) | 13% НДФЛ (автоматически) | ИП на УСН 6%: ~6% + фикс. взносы (~50к/год).<br>Самозанятость (НПД): 6% (юрлицо) / 4% (физлицо). | 6% / 4% (платит сам из полученных средств) |
| Cost-to-Company (Всего затрат компании) | Gross × 1.3 - 1.43 | Net / (1 - 0.06) ≈ Net × 1.064 (ИП УСН) | Net × 1.064 (Самозанятость) |
| Пример (Net 400к руб.) | Компания платит ~570-572к | Компания платит ~425к (ИП) / ~425к (НПД) | Компания платит ~425к |
| Экономия компании | База | ~145-150к руб./мес. (25-26%) | ~145-150к руб./мес. |
| Соц. гарантии (Больничный, Отпуск, Декрет) | Полные (оплачиваемые) | Нет (только если ИП платит взносы за себя — только больничный/пенсия, нет отпуска) | Нет (только если платит взносы добровольно) |
| Риск реквалификации (ФНС/Суд) | Нет | Высокий (при признаках труда: подчинение, режим, место, инструменты, регулярность) | Средний (НПД нельзя нанимать бывших сотрудников 2 года, лимит 2.4М/год) |
| Ответственность за ошибки/ущерб | Ограничена ТК РФ (до средней зарплаты) | Полная материальная (по ГК РФ, договор может предвидеть неограниченную) | Полная материальная |
| IP Rights (Интеллектуальная собственность) | Автоматически у работодателя (служебное изобретение) | Только по договору (обязательно пункт об исключительной лицензии/уступке прав) | Только по договору |
| Банковское обслуживание | Зарплатная карта | Расчетный счет ИП (обязателен для 6% УСН) / Карта физлица (НПД) | Карта физлица |
| Визовая поддержка / РНМ | Просто (уведомление МВД) | Сложно (нужен патент/РВП/ВНЖ, уведомление о трудовой деятельности) | Невозможно (НПД не дает прав на работу иностранцам без патента) |
2. Разбор предложенного варианта: «ГПХ на старте 2-3 мес (мин 11 мес), потом ТК РФ»
Риски для вас (Кандидата):
- «Минимум 11 месяцев» при плане 2-3 мес — противоречие. Если вы уйдете через 3 мес. (пробы/не понравилось) — нарушение договора, штрафы, споры. Если останетесь 11 мес. — вы теряете 9 мес. соц. гарантий (отпуск, больничный, стаж).
- Статус «Самозанятости (НПД)» vs ИП:
- НПД: Лимит 2.4 млн руб./год. При Net 300к+ — лимит исчерпается за 8 месяцев. Не подходит для Senior на год.
- ИП на УСН 6%: Нет лимита дохода (до 200-250М/год). Нужно открыть ИП, счет, платить фикс. взносы (~50к/год + 1% сверх 300к дохода). Оптимально для долгосрочного ГПХ.
- Риск реквалификации в Трудовые отношения (Статья 19.1 КоАП РФ, ст. 5.27 КоАП РФ):
- Признаки: вы идете на дейлики в 11:00, сидите в их Trello/Slack, используете их ноутбук/доступы, подчиняетесь PM/Founder, работаете только на них.
- Штрафы на компанию: 10-20% от суммы (минимум 50-100к) + доп. начисление взносов + пени.
- Для вас: потеря статуса ИП/НПД, перерасчет налогов, проблемы с банком (115-ФЗ).
- Отсутствие On-call компенсации / Овертайма: В ГПХ вы платите за результат, а не за время. Ночной инцидент = бесплатно, если не прописано иначе.
Риски для Компании (поэтому они предлагают ГПХ):
- Экономия ~25-30% на ФОТ (Фонде оплаты труда).
- Легко уволить (расторжение договора) без судов и выплат (если не реквалифицируют).
- Нет обязательств по отпускам/больничным/декретам.
3. Правильная стратегия переговоров (Senior Position)
Вариант А: Если вы в РФ, гражданин РФ, готовы открыть ИП (Рекомендую для долгосрочки)
> «Готов работать через ИП на УСН 6% (Доходы). Это легально, прозрачно, нет лимита 2.4М. > Условия договора (Договор Подряда/Оказания услуг): > 1. Нет режима труда: Я определяю график/место работы сам (Async-first). Дейлики — по согласованию, не обязательные. > 2. Свои инструменты: Мой ноутбук, мои лицензии (IDE, Figma Pro), мой интернет. > 3. Результат, а не процесс: Приемка по этапам (Acceptance Criteria / Demo), Акт приема-передачи работ. > 4. IP Rights: Пункт об уступке исключительных прав на код/дизайн заказчику. > 5. Ответственность: Ограниченная (не более стоимости этапа / 1 месяца вознаграждения), нет штрафов за «опоздания». > 6. Рейт: Net × 1.064 (чтобы компания платила столько же, сколько за ТК РФ Gross, но я получаю Net на руки + покрываю взносы). > 7. Минимальный срок: 3 месяца (пробный период проекта), далее — пролонгация по согласованию. Никаких «11 месяцев» без гарантий.
Вариант Б: Если не хотите ИП / Иностранный гражданин / Хотите гарантии → Только ТК РФ
> «ГПХ несет риски реквалификации для вас и потерю гарантий для меня. Давайте сразу ТК РФ (Срочный трудовой договор на 1-3 года). > Мой запрос к пакету: > 1. Base Salary (Gross) = Market Rate (P75). > 2. Equity / Options (компенсация риска стартапа). > 3. Доп. страховка (ДМС + Стома + Жизнь) с первого дня. > 4. Обучение / Конференции / Книги (бюджет 150-200к/год). > 5. Гибридный график / Remote (фиксировано в доп. соглашении). > 6. Sign-on Bonus (если Base ниже маркета).»
Вариант В: Компромисс «ГПХ через ИП 3 мес -> ТК РФ» (Только если выгодно финансово сейчас)
> «Согласен на ИП (УСН 6%) на 3 месяца (пилотный период), но: > 1. Рейт = Net × 1.15 (премия за риск, отсутствие отпуска/больничного, налогничного, покрытие взносов, бухгалтерии). > 2. Четкий критерий перехода на ТК РФ: Успешный запуск MVP / прохождение аудита / конец квартала. > 3. Гарантия перехода: В договоре прописано: «По итогам 3 месяцев стороны заключают Трудовой договор на неопределенный срок с сохранением условий». > 4. **Компания оплачивает открытие ИП / печати / бухгалтера / юриста на старте».
4. Чек-лист безопасности договора ГПХ/ИП (Must Have в тексте)
1. ПРЕДМЕТ: Оказание услуг по разработке ПО / Техническое руководство (не "выполнение работы программистом").
2. РЕЖИМ: Исполнитель определяет режим, место и время работы самостоятельно. Заказчик не устанавливает режим труда.
3. ИНСТРУМЕНТЫ: Исполнитель использует собственное оборудование, лицензии, ПО.
4. ПРИЕМКА: Поэтапная. Акт приема-передачи услуг (КС-2 / Универсальный передаточный документ) подписывается в течение 3 рабочих дней после демо. Считается принятым, если заказчик не прислал претензий за 5 дней.
5. ИНТЕЛЛЕКТУАЛЬНЫЕ ПРАВА: Исключительное право на результаты (код, архитектура, доки) переходит к Заказчику с момента оплаты этапа (или полной оплаты). Исполнитель гарантиет чистоту прав.
6. ОТВЕТСТВЕННОСТЬ: Штрафы/пени ТОЛЬКО за просрочку оплаты Заказчиком. Ответственность Исполнителя за ущерб ограничена суммой вознаграждения за текущий месяц/этап. Нет солидарной ответственности.
7. НДС: Цена включает НДС (если ИП на УСН "Доходы" - НДС не взимается, пишем "Без НДС"). Если общая - отдельной строкой.
8. РАСТОРЖЕНИЕ: Любой стороной с уведомлением за 14 календарных дней. При расторжении Заказчиком без вины Исполнителя - оплата за фактически выполненные этапы + компенсация 50% от стоимости следующего этапа.
9. КОНФИДЕНЦИАЛЬНОСТЬ: NDA на 3 года после окончания.
10. НЕАБСОЛЮТНОСТЬ: Исполнитель имеет право привлекать субисполнителей (с согласованием) и работать с другими заказчиками (не конкурентами).
Резюме (TL;DR для разговора с Founder'ом/HR)
> «Предложение «ГПХ на 11 месяцев с опцией ухода за 3» выглядит как попытка переложить риски на меня, не давая бонусов. > > Мой выбор — один из двух: > 1. ТК РФ (Срочный на 1-3 года) — если вы готовы дать маркетный Gross + Equity + ДМС + Обучение. Это честно, безопасно, дает мне гарантии, вам — лояльность и IP-безопасность. > 2. ИП на УСН 6% (Договор подряда) — если нужно оптимизировать налоги сейчас. > * Рейт: Net × 1.15 (премия за риски). > * Срок: 3 месяца (пилот), без автопролонга на 11 мес. > * Условия: Нет режима труда, свои инструменты, приемка по Актам, ограниченная ответственность. > * Точка выхода: Через 3 мес. — либо ТК РФ на тех же условиях (Base + Equity), либо расходимся по Актам. > > Вариант «ГПХ физлицо / Самозанятость» не рассматриваю — лимит 2.4М/год закрывает Senior-уровень за 8 месяцев, риск реквалификации максимален. > > Готов подписать любой из этих двух вариантов сегодня, если условия зафиксированы».
Вопрос 30. Какие варианты оформления рассматриваются: ТК РФ или ГПХ?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:26:37"/>
Ответ собеседника: правильный. Предлагают оба варианта. Кандидат склоняется к ГПХ на старте (2-3 месяца, минимум 11 месяцев по договору), чтобы снизить риски для сторон, потом возможно ТК РФ.
Правильный ответ:
Выбор между ТК РФ (Трудовой договор) и ГПХ (Гражданско-правовой договор / Самостоятельность / ИП) — это не просто административная формальность, а стратегическое финансовое и правовое решение, влияющее на налоговую нагрузку, социальные гарантии, риски проверок (ФНС/Трудовая инспекция) и долгосрочное планирование.
Для Senior/Tech Lead уровня дохода (обычно > 300-500к руб./мес. на руки) разница в нетто и рисках колоссальна.
1. Сравнительная таблица (Senior View)
| Параметр | ТК РФ (Трудовой договор) | ГПХ / ИП (Услуги / Подряд) | ГПХ / Физлицо (Самозанятость / НПД) |
|---|---|---|---|
| Налоговая нагрузка (Работодатель) | ~30-43% сверх Gross (Взносы: Пенс 22%, ФОМС 5.1%, ФСС 2.9%, НС 0.6-8.5%) | 0% (если ИП/Самозанятость) или 30% (если физлицо без статуса) | 0% (платит исполнитель: 6% физ / 4% юр) |
| Налоговая нагрузка (Исполнитель) | 13% НДФЛ (автоматически) | ИП на УСН 6%: ~6% + фикс. взносы (~50к/год).<br>Самозанятость (НПД): 6% (юрлицо) / 4% (физлицо). | 6% / 4% (платит сам из полученных средств) |
| Cost-to-Company (Всего затрат компании) | Gross × 1.3 - 1.43 | Net / (1 - 0.06) ≈ Net × 1.064 (ИП УСН) | Net × 1.064 (Самозанятость) |
| Пример (Net 400к руб.) | Компания платит ~570-572к | Компания платит ~425к (ИП) / ~425к (НПД) | Компания платит ~425к |
| Экономия компании | База | ~145-150к руб./мес. (25-26%) | ~145-150к руб./мес. |
| Соц. гарантии (Больничный, Отпуск, Декрет) | Полные (оплачиваемые) | Нет (только если ИП платит взносы за себя — только больничный/пенсия, нет отпуска) | Нет (только если платит взносы добровольно) |
| Риск реквалификации (ФНС/Суд) | Нет | Высокий (при признаках труда: подчинение, режим, место, инструменты, регулярность) | Средний (НПД нельзя нанимать бывших сотрудников 2 года, лимит 2.4М/год) |
| Ответственность за ошибки/ущерб | Ограничена ТК РФ (до средней зарплаты) | Полная материальная (по ГК РФ, договор может предвидеть неограниченную) | Полная материальная |
| IP Rights (Интеллектуальная собственность) | Автоматически у работодателя (служебное изобретение) | Только по договору (обязательно пункт об исключительной лицензии/уступке прав) | Только по договору |
| Банковское обслуживание | Зарплатная карта | Расчетный счет ИП (обязателен для 6% УСН) / Карта физлица (НПД) | Карта физлица |
| Визовая поддержка / РНМ | Просто (уведомление МВД) | Сложно (нужен патент/РВП/ВНЖ, уведомление о трудовой деятельности) | Невозможно (НПД не дает прав на работу иностранцам без патента) |
2. Разбор предложенного варианта: «ГПХ на старте 2-3 мес (мин 11 мес), потом ТК РФ»
Риски для вас (Кандидата):
- «Минимум 11 месяцев» при плане 2-3 мес — противоречие. Если вы уйдете через 3 мес. (пробы/не понравилось) — нарушение договора, штрафы, споры. Если останетесь 11 мес. — вы теряете 9 мес. соц. гарантий (отпуск, больничный, стаж).
- Статус «Самозанятости (НПД)» vs ИП:
- НПД: Лимит 2.4 млн руб./год. При Net 300к+ — лимит исчерпается за 8 месяцев. Не подходит для Senior на год.
- ИП на УСН 6%: Нет лимита дохода (до 200-250М/год). Нужно открыть ИП, счет, платить фикс. взносы (~50к/год + 1% сверх 300к дохода). Оптимально для долгосрочного ГПХ.
- Риск реквалификации в Трудовые отношения (Статья 19.1 КоАП РФ, ст. 5.27 КоАП РФ):
- Признаки: вы идете на дейлики в 11:00, сидите в их Trello/Slack, используете их ноутбук/доступы, подчиняетесь PM/Founder, работаете только на них.
- Штрафы на компанию: 10-20% от суммы (минимум 50-100к) + доп. начисление взносов + пени.
- Для вас: потеря статуса ИП/НПД, перерасчет налогов, проблемы с банком (115-ФЗ).
- Отсутствие On-call компенсации / Овертайма: В ГПХ вы платите за результат, а не за время. Ночной инцидент = бесплатно, если не прописано иначе.
Риски для Компании (поэтому они предлагают ГПХ):
- Экономия ~25-30% на ФОТ (Фонде оплаты труда).
- Легко уволить (расторжение договора) без судов и выплат (если не реквалифицируют).
- Нет обязательств по отпускам/больничным/декретам.
3. Правильная стратегия переговоров (Senior Position)
Вариант А: Если вы в РФ, гражданин РФ, готовы открыть ИП (Рекомендую для долгосрочки)
> «Готов работать через ИП на УСН 6% (Доходы). Это легально, прозрачно, нет лимита 2.4М. > Условия договора (Договор Подряда/Оказания услуг): > 1. Нет режима труда: Я определяю график/место работы сам (Async-first). Дейлики — по согласованию, не обязательные. > 2. Свои инструменты: Мой ноутбук, мои лицензии (IDE, Figma Pro), мой интернет. > 3. Результат, а не процесс: Приемка по этапам (Acceptance Criteria / Demo), Акт приема-передачи работ. > 4. IP Rights: Пункт об уступке исключительных прав на код/дизайн заказчику. > 5. Ответственность: Ограниченная (не более стоимости этапа / 1 месяца вознаграждения), нет штрафов за «опоздания». > 6. Рейт: Net × 1.064 (чтобы компания платила столько же, сколько за ТК РФ Gross, но я получаю Net на руки + покрываю взносы). > 7. Минимальный срок: 3 месяца (пробный период проекта), далее — пролонгация по согласованию. Никаких «11 месяцев» без гарантий.
Вариант Б: Если не хотите ИП / Иностранный гражданин / Хотите гарантии → Только ТК РФ
> «ГПХ несет риски реквалификации для вас и потерю гарантий для меня. Давайте сразу ТК РФ (Срочный трудовой договор на 1-3 года). > Мой запрос к пакету: > 1. Base Salary (Gross) = Market Rate (P75). > 2. Equity / Options (компенсация риска стартапа). > 3. Доп. страховка (ДМС + Стома + Жизнь) с первого дня. > 4. Обучение / Конференции / Книги (бюджет 150-200к/год). > 5. Гибридный график / Remote (фиксировано в доп. соглашении). > 6. Sign-on Bonus (если Base ниже маркета).»
Вариант В: Компромисс «ГПХ через ИП 3 мес -> ТК РФ» (Только если выгодно финансово сейчас)
> «Согласен на ИП (УСН 6%) на 3 месяца (пилотный период), но: > 1. Рейт = Net × 1.15 (премия за риск, отсутствие отпуска/больничного, налогничного, покрытие взносов, бухгалтерии). > 2. Четкий критерий перехода на ТК РФ: Успешный запуск MVP / прохождение аудита / конец квартала. > 3. Гарантия перехода: В договоре прописано: «По итогам 3 месяцев стороны заключают Трудовой договор на неопределенный срок с сохранением условий». > 4. **Компания оплачивает открытие ИП / печати / бухгалтера / юриста на старте».
4. Чек-лист безопасности договора ГПХ/ИП (Must Have в тексте)
1. ПРЕДМЕТ: Оказание услуг по разработке ПО / Техническое руководство (не "выполнение работы программистом").
2. РЕЖИМ: Исполнитель определяет режим, место и время работы самостоятельно. Заказчик не устанавливает режим труда.
3. ИНСТРУМЕНТЫ: Исполнитель использует собственное оборудование, лицензии, ПО.
4. ПРИЕМКА: Поэтапная. Акт приема-передачи услуг (КС-2 / Универсальный передаточный документ) подписывается в течение 3 рабочих дней после демо. Считается принятым, если заказчик не прислал претензий за 5 дней.
5. ИНТЕЛЛЕКТУАЛЬНЫЕ ПРАВА: Исключительное право на результаты (код, архитектура, доки) переходит к Заказчику с момента оплаты этапа (или полной оплаты). Исполнитель гарантирует чистоту прав.
6. ОТВЕТСТВЕННОСТЬ: Штрафы/пени ТОЛЬКО за просрочку оплаты Заказчиком. Ответственность Исполнителя за ущерб ограничена суммой вознаграждения за текущий месяц/этап. Нет солидарной ответственности.
7. НДС: Цена включает НДС (если ИП на УСН "Доходы" - НДС не взимается, пишем "Без НДС"). Если общая - отдельной строкой.
8. РАСТОРЖЕНИЕ: Любой стороной с уведомлением за 14 календарных дней. При расторжении Заказчиком без вины Исполнителя - оплата за фактически выполненные этапы + компенсация 50% от стоимости следующего этапа.
9. КОНФИДЕНЦИАЛЬНОСТЬ: NDA на 3 года после окончания.
10. НЕАБСОЛЮТНОСТЬ: Исполнитель имеет право привлекать субисполнителей (с согласованием) и работать с другими заказчиками (не конкурентами).
Резюме (TL;DR для разговора с Founder'ом/HR)
> «Предложение «ГПХ на 11 месяцев с опцией ухода за 3» выглядит как попытка переложить риски на меня, не давая бонусов. > > Мой выбор — один из двух: > 1. ТК РФ (Срочный на 1-3 года) — если вы готовы дать маркетный Gross + Equity + ДМС + Обучение. Это честно, безопасно, дает мне гарантии, вам — лояльность и IP-безопасность. > 2. ИП на УСН 6% (Договор подряда) — если нужно оптимизировать налоги сейчас. > * Рейт: Net × 1.15 (премия за риски). > * Срок: 3 месяца (пилот), без автопролонга на 11 мес. > * Условия: Нет режима труда, свои инструменты, приемка по Актам, ограниченная ответственность. > * Точка выхода: Через 3 мес. — либо ТК РФ на тех же условиях (Base + Equity), либо расходимся по Актам. > > Вариант «ГПХ физлицо / Самозанятость» не рассматриваю — лимит 2.4М/год закрывает Senior-уровень за 8 месяцев, риск реквалификации максимален. > > Готов подписать любой из этих двух вариантов сегодня, если условия зафиксированы».
Вопрос 31. Могли бы показать продукт визуально? Я видел только сайт и вакансию.
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:28:17"/>
Ответ собеседника: правильный. Не показывали продукт, только заходил на сайт и смотрел стек в вакансии.
Правильный ответ:
Запрос демо-доступа или скринкаста продукта — это профессиональный стандарт Senior/Tech Lead. Вы не можете проектировать архитектуру, оценивать сложность задач (ту «одну фичу в приоритете») или предлагать правильные технические решения, не понимая User Flow, Domain Model и UI Density.
Отказ или невозможность показать продукт до оффера — серьезный Red Flag (или признак крайней неорганизованности). Ниже — чек-лист, что именно нужно посмотреть и почему, и как это использовать на онбординге.
1. Что именно просить посмотреть (Checklist для Demo Access)
| Артефакт | Зачем нужно Senior/Tech Lead | Что искать (Red Flags) |
|---|---|---|
| Живой доступ к Staging/Preview (логины: Recruiter / Candidate / Admin) | Понять реальные юзер-джорни, не только «счастливый путь». | Нет staging-окружения, доступы только у одного человека, «покажем потом». |
| Figma / Design System (файл, а не скриншоты) | Оценить сложность UI: таблицы, формы, модалки, адаптивность, токены (цвета, spacing, typography). | Хаос в слоях, нет Design Tokens, каждый экран уникален, нет компонентного подхода. |
| Storybook / Component Library (если есть) | Понять текущее качество FE-кода, покрытие компонентов, изоляцию. | Нет Storybook, компоненты не изолированы, нет документации пропсов. |
| API Docs (Swagger/OpenAPI) + Postman Collection | Оценить контракт: REST vs RPC, пагинация, фильтрация, ошибки, версионирование. | Нет сваггера,.swagger невалидный, поля any, нет идемпотентности. |
| DB Schema (ER-диаграмма) или Prisma/TypeORM схемы | Понять домен: связи, индексы, мягкое удаление, мультитенантность (если есть). | Нет ERD, таблицы без FK, jsonb везде вместо нормализации, нет миграций. |
| Monitoring / Sentry / Logs (доступ чтения) | Оценить стабильность: ошибки, производительность, алерты. | Сентри пустой/зашумлен, нет алертов на 5xx, нет трейсинга. |
2. Как анализировать продукт «вглубь» за 30 минут (Framework)
Если дали доступ — не просто кликайте. Проведите Technical Product Audit.
А. Domain Modeling (Ментальная карта сущностей)
Выпишите ключевые сущности и связи. Для HR Tech это обычно:
erDiagram
TENANT ||--o{ USER : "has"
USER ||--o{ VACANCY : "manages"
VACANCY ||--o{ STAGE : "pipeline"
VACANCY ||--o{ APPLICATION : "receives"
APPLICATION }|--|| CANDIDATE : "from"
CANDIDATE ||--o{ RESUME : "has"
APPLICATION ||--o{ MESSAGE : "chat"
APPLICATION ||--o{ EVENT : "timeline/audit log"
TENANT ||--o{ SETTINGS : "config (branding, integrations)"
Вопросы к модели: Есть ли мультитенантность (SaaS)? Как изолированы данные? Есть ли кастомные поля у вакансии/кандидата (EAV pattern или JSONB)?
Б. Complexity Hotspots (Точки сложности FE)
Помечайте на скриншотах/в Figma:
- Heavy Tables: Канбан-доска / Таблица кандидатов (сортировка, фильтры, колонки, drag&drow, виртуализация 10k+ строк).
- Complex Forms: Создание вакансии (шаги, условная логика, валидация cross-field, автосохранение черновиков).
- Real-time: Чат, уведомления, коллаборация (presence, typing indicators).
- File Handling: Резюме (парсинг, превью PDF/DOCX, drag&drop, прогресс бар).
- Settings/Config: Брендинг портала, интеграции (HH, LinkedIn, Telegram), права доступа (RBAC/ABAC).
В. Technical Debt Indicators (Визуальные признаки)
- Layout Shift / Jank: При загрузке списков, переключении табов.
- Inconsistent UI: Разные инпуты, кнопки, модалки на разных страницах.
- Missing States: Нет Empty State, Loading Skeletons, Error Boundaries, Optimistic UI.
- Mobile/Responsive: Адаптив ломается на 1024px / 768px.
3. Пример вопросов к Founder/CTO после просмотра (Deep Dive)
> Архитектура FE:
> 1. «Вижу сложную канбан-доску. Как сейчас реализован DnD? (React Beautiful DnD / Dnd Kit / Pragmatic DnD / нативный). Есть ли виртуализация колонок?»
> 2. «Форма вакансии — многошаговая. Как управляется состояние? (React Hook Form + Zod / Formik / XState / Redux Toolkit). Есть ли грязное чтение/запись в localStorage?»
> 3. «Есть ли Design System пакет? Как версионируются компоненты? Как выкатываются изменения в DS в продукт?»
> 4. «Как организована авторизация на FE? (RBAC на роутах, скрытие кнопок, пермишны в компонентах). Есть ли HOC/Wrapper CanAccess?»
> Интеграция с BE:
> 5. «В Swagger вижу any для фильтров. Как типизируете запросы на FE? (Orval / Hey API / ручные типы). Есть ли runtime-валидация (Zod/Valibot) входящих данных?»
> 6. «Планируете WebSockets для чата. Как будете синхронизировать состояние сообщений с TanStack Query? (Optimistic Updates + Invalidation / WebSocket cache sync)».
> Процессы: > 7. «Как выглядит Release Process? Feature Flags? Canary Deployments? Rollback strategy?» > 8. «Есть ли E2E тесты (Playwright/Cypress) на критичные пути: Login -> Create Vacancy -> Publish -> Apply?»
4. Если доступ не дали — как действовать (Risk Mitigation)
Если на собеседовании/перед оффером не показывают продукт («секретно», «нет доступа», «покажешь после ТК»):
- Не подписывайте оффер вслепую. Риск попасть в Legacy/Spaghetti/No-process слишком высок.
- Выдвиньте условие: «Готов двигаться к офферу после 1-часовой технической демо-сессии с CTO/Lead BE. Мне нужно увидеть: Staging, Figma, Swagger, Repo Structure (только структура папок), CI/CD пайплайн. Это стандартная Due Diligence для моей роли».
- Оцените реакцию:
- Адекватная: «Конечно, давай завтра в 14:00» -> Green Flag.
- Сопротивление: «Зачем тебе это, ты же просто верстать будешь» / «Наш CTO занят» / «Политика безопасности» -> Red Flag. Уходите или требуйте значительный Risk Premium (выше зарплата, короткий пробный период, легкий выход).
5. Артефакты, которые вы должны создать после просмотра (Onboarding Week 1-2)
Если вы все-таки присоединились, первые 2 недели (Read-Only Audit) должны дать эти документы в репе/вики:
PRODUCT_TECH_MAP.md
# Product Technical Map
## 1. Domain Model (Key Entities)
- Tenant (SaaS mode: yes/no)
- User (Roles: Owner, Admin, Recruiter, Viewer)
- Vacancy (Statuses, Custom Fields: JSONB/EAV)
- Candidate (Source Tracking, GDPR Consent)
- Application (Pipeline Stages, SLA Timers)
- Chat (WebSocket Channel Strategy)
## 2. FE Architecture Snapshot
- Stack: React 18, TS, Vite, RTK Query, MUI v5, React Router v6
- State: Redux Toolkit (Global UI, Auth), RTK Query (Server State), Local State (Forms)
- Design System: Internal `@company/ui-kit` (vX.Y.Z, published to npm/GH Packages)
- Component Structure: FSD (Features-Slices-Domain) / Atomic / Module-based
## 3. Complexity Heatmap
| Module | Complexity (1-5) | Tech Debt | Tests Coverage | Priority Refactor |
| :--- | :---: | :---: | :---: | :--- |
| Kanban Board | 5 | High (no virtualization, custom DnD) | 10% | **P0** |
| Vacancy Wizard | 4 | Medium (validation logic in components) | 30% | P1 |
| Candidate Profile | 3 | Low | 60% | P2 |
| Settings / Integrations | 4 | High (spaghetti code) | 5% | **P0** |
| Chat / Messages | 2 (Not implemented) | N/A | 0% | P1 (New Feature) |
## 4. Integration Contracts
- API: REST (OpenAPI 3.0, generated via Orval)
- Auth: JWT (Access 15m / Refresh 7d, HttpOnly Cookies)
- WS: Centrifugo (JWT Sub, Private Channels `chat:{appId}`)
- Storage: S3 Presigned URLs (Direct Upload)
## 5. Infrastructure & CI/CD
- Hosting: Vercel (Preview) / K8s (Prod)
- CI: GitHub Actions (Lint, Typecheck, Unit, E2E, Bundle Size)
- Monitoring: Sentry (FE), Grafana Loki (Logs), Prometheus (Metrics)
- Feature Flags: LaunchDarkly / Unleash / Custom
Резюме (TL;DR)
> «Сеньор не пишет код вслепую. Демо продукта — обязательное условие для принятия решения об оффере и для планирования первых шагов. > > Что я смотрю за 30 мин демо: > 1. Domain Model (ERD в голове) -> понимание сложности данных. > 2. UI Complexity Hotspots (Таблицы, Формы, Реал-тайм, Файлы) -> оценка нагрузки на FE. > 3. Design System / Code Quality (Figma -> Storybook -> Repo) -> оценка скорости доставки. > 4. API Contract & Infra (Swagger, CI/CD, Monitoring) -> оценка зрелости процесса. > > Если не показывают — не иду. Исключение: только если компенсация покрывает риск «Кот в мешке» (Sign-on + High Base + Easy Exit), но даже тогда — только после технической встречи с CTO/Lead BE».
Вопрос 32. Как устроена интеграция с hh.ru — через публичный API или есть договорённости?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:30:49"/>
Ответ собеседника: правильный. Используют публичный API hh.ru, специальных договорённостей нет.
Правильный ответ:
Интеграция с hh.ru (HeadHunter) — это классическая задача на OAuth 2.0 + REST API с жесткими лимитами, модерацией и специфическими бизнес-правилами. Отсутствие «специальных договоренностей» (партнерского API / Enterprise тарифа) накладывает жёсткие технические и продуктовые ограничения, которые Senior/Tech Lead должен заложить в архитектуру с первого дня.
1. Юридическая и бизнесовая модель (Public API Terms)
| Аспект | Публичный API (Текущий случай) | Партнерский / Enterprise API |
|---|---|---|
| Доступ | Регистрация приложения в Dev-портале, модерация hh.ru. | Индивидуальный договор, выделенный менеджер, SLA. |
| Лимиты (Rate Limits) | Строгие: ~120-300 req/min на client_id (зависят от эндпоинта). Превышение = 429 / Ban. | Высокие/Индивидуальные (тысячи rps), приоритетная очередь. |
| Скоупы (Scopes) | Ограниченный набор: resumes, vacancies, negotiations, messages. Нет доступа к аналитике, архивам, кастомным полям. | Расширенные скоупы, доступ к analytics, reports, webhooks на все события. |
| Брендинг / UI | Обязательное указание «Powered by hh.ru», логотип, ссылки. Строгая модерация UI при ревью приложения. | Возможность «White Label» (скрыть hh), кастомные формы. |
| Данные / GDPR / 152-ФЗ | Вы — Оператор ПДн для данных кандидатов, пришедших из hh. Согласие на обработку получает hh, вы должны его подтверждать/логировать. | Четкие DPA (Data Processing Agreement), инструкции по удалению (Right to be forgotten). |
| Стоимость | Бесплатно (для базовые лимиты) или фиксированные тарифы за доступ к резюме/вакансии. | Индивидуальные коммерческие условия (Revenue share / Fixed Fee). |
Вывод: На публичном API вы гость. hh.ru может отключить доступ в любой момент за нарушение правил (спам, превышение лимитов, некорректный UI, хранение запрещенных данных). Архитектура должна быть устойчива к отключению/деградации.
2. Техническая реализация (Architecture Patterns)
А. OAuth 2.0 Flow (Authorization Code Grant + PKCE)
sequenceDiagram
participant User
participant Frontend (Your App)
participant Backend (Your API)
participant HH (hh.ru OAuth Server)
participant HH API
User->>Frontend: Click "Connect HH"
Frontend->>Backend: Request Auth URL (state, code_challenge)
Backend->>Frontend: Redirect URL (hh.ru/oauth/authorize?...)
Frontend->>User: Redirect to HH Login/Consent
User->>HH: Login + Grant Consent
HH->>Frontend: Redirect back (?code=xxx&state=yyy)
Frontend->>Backend: POST /auth/hh/callback {code, state}
Backend->>HH: POST /oauth/token {code, code_verifier, client_secret}
HH->>Backend: {access_token, refresh_token, expires_in}
Backend->>DB: Save Tokens (Encrypted!) + UserID + Scopes
Backend->>Frontend: Set Auth Cookie / Return JWT
Критичные детали:
- PKCE (RFC 7636): Обязателен для публичных клиентов (SPA/Mobile), рекомендуется для Backend-for-Frontend.
- State Parameter: Защита от CSRF (привязка к сессии пользователя).
- Хранение токенов:
access_token(живет 2-4 часа) иrefresh_token(живет вечно, пока не отозван) — только в зашифрованном виде (AES-256 / Vault / Sealed Secrets). Никаких логов с токенами. - Авто-рефреш: Middleware/Interceptor на BE, который перехватывает 401 от hh.ru, делает
POST /oauth/tokenсrefresh_token, обновляет БД, ретраит запрос.
Б. Rate Limiting & Throttling (Client-Side Protection)
Нельзя полагаться на 429 от hh.ru. Нужен Token Bucket / Leaky Bucket на вашем шлюзе (API Gateway / Sidecar / Library).
// Пример логики Rate Limiter на Go (упрощенно)
type HHRateLimiter struct {
tokens chan struct{}
refill time.Duration // e.g., 120 req/min = 1 token per 500ms
}
func NewHHRateLimiter(rpm int) *HHRateLimiter {
rl := &HHRateLimiter{
tokens: make(chan struct{}, rpm),
refill: time.Minute / time.Duration(rpm),
}
// Initial fill
for i := 0; i < rpm; i++ { rl.tokens <- struct{}{} }
// Refill goroutine
go func() {
ticker := time.NewTicker(rl.refill)
for range ticker.C {
select { case rl.tokens <- struct{}{} : default: }
}
}()
return rl
}
func (rl *HHRateLimiter) Take(ctx context.Context) error {
select {
case <-rl.tokens: return nil
case <-ctx.Done(): return ctx.Err()
}
}
Применяйте отдельные лимитеры для разных эндпоинтов (vacancies, resumes, negotiations имеют разные квоты).
В. Идемпотентность и Очереди (Outbox Pattern)
Все мутирующие операции (создание вакансии, отклик, отправка сообщения, изменение статуса) нельзя делать синхронно в HTTP-хендлере.
flowchart LR
A[User Action] --> B[API Handler]
B --> C[DB Transaction: Save Entity + Write to Outbox Table]
C --> D[Return 202 Accepted]
D --> E[Relay / CDC (Kafka/NATS/RabbitMQ)]
E --> F[HH Worker Pool]
F --> G[Rate Limiter]
G --> H[HH API]
H --> I{Success?}
I -- Yes --> J[Mark Outbox Done]
I -- 429/5xx --> K[Exponential Backoff + Retry]
I -- 401 --> L[Refresh Token -> Retry]
I -- 403/404/422 --> M[Alert + DLQ / Manual Review]
Г. Вебхуки (Webhooks) vs Поллинг (Polling)
- Публичный API: Поддерживает вебхуки на события:
vacancy_published,resume_received,negotiation_created,message_added. - Проблема: hh.ru не гарантирует доставку (нет ретраев с бэкоффом в старом API, в новом — есть, но ненадежные). Нет гарантии порядка.
- Решение: Hybrid Approach.
- Принимаем вебхук -> Валидируем подпись (
X-Hh-SignatureHMAC SHA256) -> Кладем в очередь обработки -> Возвращаем 200 OK мгновенно (< 100ms). - Reconciliation Job (Cron каждые 5-15 мин): Достает из hh.ru списки измененных сущностей (
updated_at/sinceпараметры) и сверяет с локальным состоянием. Это страховка от потерянных вебхуков.
- Принимаем вебхук -> Валидируем подпись (
3. Доменные сложности (Business Logic Traps)
| Проблема | Описание | Решение |
|---|---|---|
| Синхронизация статусов | У hh свои статусы отклика (new, invited, rejected, archived). У вас — свои (screening, interview, offer, hired). | Mapping Table + State Machine. Не храните hh-статус как свой. Маппинг: invited -> interview_scheduled. Обрабатывайте rejected от hh как триггер закрытия в вашей системе. |
| Дубликаты вакансий/резюме | Пользователь может опубликовать вакансию и у вас, и в hh вручную. Кандидат откликается и туда, и сюда. | Deduplication Key: hh_vacancy_id на вашей вакансии. hh_resume_id / hh_applicant_id на кандидате/отклике. При входящем вебхуке/поллинге — UPSERT по внешнему ID. |
| Парсинг резюме (CV Parsing) | hh.ru отдает резюме в своем JSON-схеме (структурированно: блоки опыт, образование, навыки). Это не PDF. | Не парсьте PDF. Используйте структурированные данные fields[] из API. Мапьте на свою схему CandidateProfile. Сохраняйте raw_hh_json для аудита/репарсинга. |
| Файлы/Вложения | Фото, портфолио, сопроводительные письма — доступны по временным ссылкам (подписанные URL, TTL ~10-30 мин). | Не храните бинарники в БД. Скачивайте -> S3 (MinIO) -> Сохраняйте s3_key в своем объекте. Обрабатывайте 404/403 на ссылках. |
| Мультитенантность (SaaS) | У вас 10 клиентов. У каждого свои client_id/client_secret в hh? Или один общий? | Один client_id на ваше приложение. В токенах (access_token) зашит owner_id (ID работодателя в hh). При вызове API от имени клиента — используйте токен того клиента, который авторизовал интеграцию. Изоляция на уровне токенов. |
4. Безопасность и Комплаенс (Security & Compliance)
- Шифрование токенов в БД:
pgcrypto/Vault Transit/ Application-level encryption (AES-GCM). Ключ ротации раз в 90 дней. - Minimal Scopes: Запрашивайте только
vacancies,negotiations,messages. Не проситеresumes, если не делаете поиск по базе hh. - Webhook Signature Verification: Обязательно.
HMAC_SHA256(payload, client_secret) == X-Hh-Signature. - Audit Log: Логируйте каждый исходящий запрос к hh (method, url, status, latency, user_id, tenant_id) и каждый входящий вебхук. Без тела (PII), только метаданные.
- GDPR / 152-ФЗ / Right to be Forgotten: Если кандидат удалил резюме в hh / отозвал согласие -> hh пришлет вебхук
resume_deleted/consent_revoked. Вы обязаны каскадно удалить/анонимизировать данные у себя за 24-72ч.
5. Мониторинг и Observability (SLO/SLI)
Дашборд "HH Integration Health" должен содержать:
| Метрика | SLO (Target) | Alert Threshold |
|---|---|---|
| API Availability (Success Rate) | > 99.5% (исключая 4xx клиента) | < 99% за 5 мин -> PagerDuty |
| Latency (p99) | < 2000 ms | > 5000 ms -> Warning |
| Rate Limit Usage | < 70% квоты | > 85% -> Scale workers / Pause non-critical sync |
| Token Refresh Success Rate | 100% | Any failure -> Critical (токен протух/отозван) |
| Webhook Processing Lag | < 30 sec | > 2 min -> Check Worker Pool |
| Reconciliation Drift | 0 mismatches | > 0 -> Manual Sync Trigger |
| Outbox Queue Depth | < 100 | > 1000 -> Backlog Alert |
6. План миграции на Partner API (Future Proofing)
Если бизнес вырастет и публичного API станет мало (лимиты, нужны аналитические отчеты, белый ярлык):
- Абстракция
HHClient: Весь код работает с интерфейсомHHClientInterface(методы:PostVacancy,GetResumes,SendMessage). - Две реализации:
PublicHHClient(текущая) иPartnerHHClient(будущая). - Feature Flag:
hh_integration_version: public | partner. - Миграция: Включаем флаг для нового тенанта -> тестируем -> роллбаутим при проблемах.
Резюме (TL;DR для разговора с CTO/Founder)
> «На публичном API hh.ru мы не контролируем инфраструктуру. Моя задача — построить устойчивый к деградации слой (Anti-Corruption Layer).
>
> Ключевые решения в коде:
> 1. Outbox Pattern + Worker Pool для всех записей (гарантия доставки, ретраи, бэкофф).
> 2. Локальный Rate Limiter (Token Bucket) — защита от 429 и бана.
> 3. Hybrid Sync: Вебхуки (реал-тайм) + Реконсилиация по Cron (гарантия консистентности).
> 4. Шифрование токенов (AES-GCM) + Авто-рефреш — безопасность и бесшовный UX.
> 5. Маппинг сущностей/статусов — изоляция нашей доменной модели от изменений у hh.
> 6. Observability (SLO на Lag, Drift, Rate Usage) — видимость проблемы до жалоб пользователей.
>
> Риск: hh.ru может урезать лимиты или запретить доступ.
> Митигация: Архитектура позволяет отключить интеграцию за 1 Feature Flag, оставив ручное управление вакансиями/кандидатами в нашем интерфейсе. План миграции на Partner API заложен в абстракцию HHClient.»
Вопрос 33. Откуда берутся резюме и как происходит парсинг/агрегация?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:31:21"/>
Ответ собеседника: правильный. Парсят резюме из множества источников (Avito, Telegram, LinkedIn), нормализуют в единый формат, кладут в OpenSearch для быстрого поиска.
Правильный ответ:
Сбор, парсинг и агрегация резюме — это Data Engineering Pipeline с жесткими требованиями к качеству данных (Data Quality), легальности (GDPR/152-ФЗ), идемпотентности и поисковой релевантности. Ответ кандидата верен по высокому уровню, но на позиции Tech Lead нужно понимать «кухню» каждого этапа.
1. Источники данных (Data Sources) — Классификация по типу доступа
| Тип источника | Примеры | Способ получения | Сложность | Риски / Ограничения |
|---|---|---|---|---|
| Официальные Партнерские API | hh.ru, SuperJob, LinkedIn (Partner Program), Авито (API для бизнеса) | OAuth2 / API Key, Webhooks, Bulk Export | Низкая (структурированный JSON, схема, поддержка) | Лимиты (Rate Limits), стоимость, зависимость от ТОС, модерация UI. |
| Публичные API / Открытые данные | Telegram Bot API (публичные каналы/боты), GitHub API, StackOverflow, Habr Career | REST / GraphQL | Средняя | Нестабильность схем, отсутствие вебхуков (полинг), строгие лимиты. |
| Скриппинг / Парсинг HTML (Scraping) | LinkedIn (без партнерки), Telegram (веб-версия/клиенты), Нишные борды, Сайты компаний (Career pages) | Headless Browser (Playwright/Puppeteer/Chrome DevTools Protocol), HTTP-клиенты + парсинг DOM | Высокая | Правовые риски (ToS violation, CFAA, 152-ФЗ), защита от ботов (Cloudflare, Akamai, Fingerprinting), частые изменения верстки, необходимость прокси/резидентных IP. |
| Входящие потоки (Inbound) | Отклики на вакансии (Email, Webhook от АТС), Резюме из формы на сайте, Резюме от рекрутеров (Drag&Drop PDF/DOCX) | Webhook, SMTP/IMAP парсинг, Multipart Upload | Низкая | Неструктурированные данные, спам, дубликаты, разные форматы. |
| Покупка/Обмен базами (Data Partnerships) | Агентства, аутсорсинговые фирмы, агрегаторы (напр. для масс-найма) | SFTP/FTPS (CSV/XML/JSON дампы), S3 Exchange, API | Средняя | Юридическая чистота (согласия субъектов ПДн), качество данных (устаревшие контакты), дедупликация. |
> Важно для LinkedIn/Telegram: Официально парсить LinkedIn без партнерства нельзя (HiQ Labs v. LinkedIn — судебные прецеденты, но ToS запрещает). Telegram позволяет парсить публичные каналы через Bot API / MTProto, но масштабный сбор требует Userbot-ферм (риск банов). Senior позиция: Строим продукт на легальных источниках (API, Inbound, Partnerships), скрейпинг — только как временный хак или для ниш, где нет API, с пониманием рисков.
2. Архитектура пайплайна (ETL / ELT Pipeline)
Рекомендуемая архитектура: Event-Driven, Micro-batch / Streaming с разделением на этапы (Separation of Concerns).
flowchart LR
subgraph Ingestion [Ingestion Layer]
A1[API Clients<br/>hh, Avito, Partners]
A2[Scraping Workers<br/>Playwright + Proxy Pool]
A3[Inbound Handlers<br/>Email, Webhook, Upload]
A4[Batch Importers<br/>SFTP, S3, CSV/JSON]
end
subgraph Transport [Transport / Buffer]
B[Kafka / NATS JetStream / Redis Streams<br/>Topic: raw_resumes]
end
subgraph Processing [Processing Layer (Stateless Workers)]
C1[Parser Service<br/>PDF/DOCX/HTML -> Structured JSON]
C2[PII Detector & Anonymizer<br/>Presidio / Regex / NER]
C3[Normalizer & Mapper<br/>Source Schema -> Canonical Model]
C4[Deduplication Service<br/>Fuzzy Match + SimHash / MinHash]
C5[Enrichment Service<br/>Skills Taxonomy, Geo-coding, Salary Benchmarks]
end
subgraph Storage [Storage Layer]
D1[(PostgreSQL<br/>Canonical Profile + Relations<br/>Source of Truth)]
D2[(OpenSearch / Elasticsearch<br/>Search Index + Analytics)]
D3[(S3 / MinIO<br/>Raw Files: PDF, HTML, Source JSON<br/>Data Lake)]
D4[(Redis<br/>Cache: Embeddings, Recent Views)]
end
subgraph Observability [Observability]
E[Metrics: Lag, Error Rate, DQ Scores<br/>Alerts: Schema Drift, PII Leak]
end
Ingestion --> Transport
Transport --> Processing
Processing --> Storage
Storage --> Observability
3. Парсинг документов (Parser Service) — Детали реализации
Вход: Бинарный файл (PDF, DOCX, DOC, RTF, TXT, HTML) или Raw JSON от API.
Выход: Каноническая JSON-схема (CandidateProfile).
Стек инструментов (Best Practices 2024):
- PDF:
pdfplumber(таблицы/позиции),pymupdf(fitz — скорость),marker(LLM-based, для сложных макетов). - DOCX:
python-docx+lxml(прямой парсинг XMLword/document.xml— надежнее для таблиц/стилей). - OCR (сканы/фото):
Tesseract/PaddleOCR/Surya/ коммерческие (AWS Textract, Azure Document Intelligence, Google Document AI) — только для сканов, не для цифровых PDF. - LLM-парсинг (State of the Art): Функция
extract_resume(text) -> JSONсjson_schema(OpenAI GPT-4o / Llama 3 / Mistral / локальные vLLM/Ollama). Дает лучшее качество извлечения скиллов, дат, компаний из неструктурированного текста, но дороже и медленнее. Гибридный подход: Правила (Regex/Heuristics) для структуры + LLM для нормализации скиллов и извлечения сущностей (NER).
Каноническая схема (Canonical Model) — Пример:
{
"source_id": "hh_12345",
"source_type": "hh_api",
"external_url": "https://hh.ru/resume/...",
"received_at": "2024-05-20T10:00:00Z",
"raw_storage_ref": "s3://bucket/raw/hh/12345.json",
"profile": {
"full_name": "Ivan Ivanov",
"contacts": [{"type": "email", "value": "ivan@mail.com", "verified": false}, {"type": "phone", "value": "+7900...", "verified": false}],
"location": {"city": "Moscow", "region": "Moscow", "country": "RU", "lat": 55.75, "lon": 37.61, "relocation": true, "remote": true},
"position": "Senior Go Developer",
"salary_expectation": {"currency": "RUB", "amount": 350000, "period": "month", "net": true},
"experience": [
{"company": "Yandex", "role": "Backend Developer", "start": "2020-01", "end": "2023-06", "description": "Highload...", "skills": ["Go", "gRPC", "ClickHouse", "Kubernetes"]}
],
"skills": [
{"name": "Go", "level": "expert", "years": 5, "source": "extracted"},
{"name": "PostgreSQL", "level": "advanced", "years": 4, "source": "extracted"}
],
"education": [{"institute": "MIPT", "faculty": "CS", "degree": "master", "year": 2019}],
"languages": [{"lang": "ru", "level": "native"}, {"lang": "en", "level": "b2"}],
"tags": ["backend", "highload", "microservices"],
"employment_types": ["full_time", "remote"],
"active_job_search": true
},
"embeddings": {"skills_vector": [0.1, -0.5, ...], "profile_vector": [...]},
"quality_score": 0.92,
"pii_flags": {"has_phone": true, "has_email": true, "anonymized_version_id": "anon_abc"}
}
4. Нормализация и Таксономия (Normalization & Taxonomy)
Самая большая проблема — «Java» != «Java 17» != «J2EE» != «Spring Boot». Решение: Внутренняя таксономия навыков (Skills Taxonomy / Knowledge Graph).
- Словарь синонимов (Synonym Dictionary): Маппинг сырых строк на канонические ID.
Go,Golang,GoLang,Go 1.22->skill_go(ID: 101).Postgres,PostgreSQL,PG,psql->skill_pg(ID: 205).
- Иерархия (Ontology):
Go->Backend Languages->Programming Languages.Kubernetes->Container Orchestration->DevOps Tools. - Версионирование таксономии: Хранить в Git/DB с версией. При обновлении — переиндексация (Re-index) в OpenSearch.
- Автоматическое пополнение: LLM извлекает новые скиллы из описаний -> Человек модератор подтверждает -> В таксономию.
5. Дедупликация (Deduplication) — Critical Path
Один кандидат = 5 профилей (hh, LinkedIn, Telegram, Upload, Email). Стратегия:
- Exact Match:
email/phone(нормализованный E.164) /linkedin_url/github_url/hh_id. -> Merge instantly. - Fuzzy Match (Blocking + Scoring):
- Blocking: Фамилия + Первая буква имени + Город / Навык.
- Scoring: Jaro-Winkler (Имя), Levenshtein (Компании), Cosine Similarity (Эмбеддинги профиля).
- Threshold: > 0.92 -> Auto-merge. 0.75-0.92 -> Queue для ручной проверки (UI для рекрутера: «Это одно лицо?»).
- SimHash / MinHash: Для детекта почти одинаковых текстов резюме (копипаста).
- Golden Record (Master Data Management): Создается сущность
Candidate(Master), к которой линкуютсяSourceProfile(Slave). Все поисковые запросы идут поCandidate(агрегированные навыки, лучшие контакты, свежая зарплата).
6. Поиск и OpenSearch / Elasticsearch (Search & Indexing)
Кандидат упомянул OpenSearch — правильный выбор для полнотекстового поиска + фильтров + аналитики.
Модель индекса (Index Mapping):
PUT candidates
{
"settings": {
"analysis": {
"analyzer": {
"ru_en_analyzer": { "tokenizer": "standard", "filter": ["lowercase", "russian_stop", "russian_stemmer", "english_stop", "english_stemmer", "asciifolding"] },
"skill_analyzer": { "tokenizer": "keyword", "filter": ["lowercase", "asciifolding"] }
}
}
},
"mappings": {
"properties": {
"candidate_id": { "type": "keyword" },
"source_profiles": { "type": "nested", "properties": { "source": {"type": "keyword"}, "source_id": {"type": "keyword"}, "received_at": {"type": "date"}, "quality_score": {"type": "float"} }},
"full_name": { "type": "text", "analyzer": "ru_en_analyzer", "fields": {"raw": {"type": "keyword"}} },
"position": { "type": "text", "analyzer": "ru_en_analyzer", "fields": {"suggest": {"type": "completion"}} },
"skills": { "type": "nested", "properties": { "skill_id": {"type": "keyword"}, "name": {"type": "text", "analyzer": "skill_analyzer"}, "level": {"type": "keyword"}, "years": {"type": "integer"} }},
"experience": { "type": "nested", "properties": { "company": {"type": "keyword"}, "role": {"type": "text"}, "start_date": {"type": "date"}, "end_date": {"type": "date"}, "skills": {"type": "keyword"} }},
"location": { "type": "object", "properties": { "city": {"type": "keyword"}, "geo": {"type": "geo_point"}, "relocation": {"type": "boolean"}, "remote": {"type": "boolean"} }},
"salary_expectation": { "type": "object", "properties": { "amount": {"type": "long"}, "currency": {"type": "keyword"}, "net": {"type": "boolean"} }},
"embeddings": { "type": "dense_vector", "dims": 768, "index": true, "similarity": "cosine" }, // Для Semantic Search (RAG)
"updated_at": { "type": "date" },
"active_job_search": { "type": "boolean" }
}
}
}
Типы поиска:
- Structured/Filtered:
skills.skill_id: "go" AND location.city: "moscow" AND salary.amount <= 400000. - Full-Text (BM25): Поиск по
position,experience.description,skills.nameс бустами. - Semantic / Vector Search (kNN):
embeddings-> Поиск по смыслу («разработчик высоконагруженных систем» найдет Go/Rust/Java Highload даже без точных скиллов). Гибридный поиск (Hybrid Search: BM25 + kNN с RRF — Reciprocal Rank Fusion). - Aggregations (Analytics): Фасеты по скиллам, зарплатам, локациям, источникам для дашбордов рекрутеров.
7. Data Quality (DQ) и SLA
| Метрика | Target | Мониторинг |
|---|---|---|
| Freshness (Lag) | API/Webhook: < 1 мин. Скрейпинг/Батч: < 1 час. | max(received_at) - now() per source. |
| Parse Success Rate | > 99.5% (PDF/DOCX), > 99.9% (API JSON) | Alert на < 99%. Dead Letter Queue (DLQ) для ручного разбора. |
| Field Completeness | skills > 90%, contacts > 80%, experience > 95% | Dashboard по источникам. |
| Duplicate Rate | < 5% входящих = дубликаты | Метрика dedup_merged_total / ingested_total. |
| PII Leakage | 0 утечек в логах/поиске | Автотесты: поиск regex email/phone в OpenSearch _source (должен быть только в contacts с флагом). |
| Schema Drift Detection | 0 неожиданных полей | Контракт-тестирование (Pact / Schemathesis) для API источников. |
8. Легальность и Комплаенс (Legal & Compliance) — Must Have для Senior
- Закон 152-ФЗ / GDPR:
- Основание обработки: Согласие субъекта (при загрузке резюме у вас) / Заинтересованность оператора (публичные источники — осторожно, нужна ДОП - оценка последствий).
- Право на забвение: API
DELETE /candidates/{id}-> Каскадное удаление из PG, OpenSearch, S3, Кэшей, DLQ за 72 часа. Логирование факта удаления. - Локализация данных (152-ФЗ): БД с ПДн (PG, OpenSearch) — только в РФ. S3 — только в РФ. Никаких.cloudflare Workers / Vercel Edge для обработки ПДн.
- ToS источников:
- hh.ru / Авито: Только через API, хранение по их правилам (обычно кэш до 30 дней или пока актуально).
- LinkedIn: Категорически запрещено скрейпить и хранить без партнерства. Используем только
Inbound(кандидат сам прислал) илиPartner API. - Telegram: Публичные каналы — можно (Bot API), но массовый сбор через Userbot — риск бана.
- Безопасность ПДн: Шифрование в покое (LUKS / Cloud KMS / pgcrypto), шифрование в движении (mTLS), маскирование в логах/метриках, разграничение доступа (RBAC: рекрутер видит только свою воронку, админ — все).
Резюме (TL;DR для архитектора)
> «Мы строим Data Platform для рекрутинга.
>
> Источники: Приоритет — Официальные API (hh, Avito, Partners) + Inbound (Email/Upload/Webhook). Скрейпинг — только легальные ниши с прокси-пулом и браузерной фермой (Playwright), изолированная от ядра.
>
> Пайплайн: Kafka -> Stateless Workers (Parse -> PII -> Normalize -> Dedup -> Enrich -> Embed) -> Dual Write (PG = Source of Truth, OpenSearch = Search/Analytics, S3 = Raw Lake).
>
> Ключевые сложности, которые решаю:
> 1. Таксономия скиллов (внутренний KG + LLM-нормализация) — основа релевантного поиска.
> 2. Дедупликация (Golden Record) — Exact match по контактам + Fuzzy/Embedding match для профилей без контактов.
> 3. Hybrid Search в OpenSearch (BM25 + kNN/RRF) + вложенные документы (nested) для опыта/скиллов.
> 4. Compliance by Design: 152-ФЗ (локализация, право на удаление), ToS источников, шифрование ПДн, аудит.
> 5. Observability: DQ-метрики (Completeness, Freshness, Duplicate Rate) как first-class citizens, алерты на Drift схем источников.
>
> Это позволяет рекрутерам находить релевантных людей за секунды, а бизнесу — масштабировать источники без переписывания ядра».
Вопрос 34. Как работает поиск кандидатов: запрос парсится на бэкенде, потом ищет в OpenSearch?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:32:13"/>
Ответ собеседника: правильный. Да: пост-запрос на бэкенд, NLP определяет профессию/опыт/отрасли, формирует фильтры, ищет в OpenSearch, возвращает кандидатов с AI-саммари и рекомендацией.
Правильный ответ:
Это классическая Search & Discovery архитектура с этапом Query Understanding (NLP), гибридным поиском (Hybrid Search: BM25 + kNN) и Re-ranking / LLM-постобработкой. На уровне Tech Lead важно не просто «ищет в OpenSearch», а как именно строится DSL-запрос, как решаются проблемы релевантности, производительности и качества саммари.
1. Общая схема потока (Request Flow)
sequenceDiagram
participant FE as Frontend (Recruiter UI)
participant API as Search API (Go)
participant NLP as Query Understanding Service (Python/Go)
participant OS as OpenSearch Cluster
participant LLM as LLM Gateway (RAG)
participant Cache as Redis (Query Cache / Embedding Cache)
FE->>API: POST /api/v1/search/candidates {query: "Senior Go разработчик Москва Kubernetes", filters: {...}, page: 1, size: 20}
API->>NLP: gRPC/HTTP ParseQuery(query)
NLP-->>API: StructuredIntent {role: "Backend Developer", skills: ["Go", "Kubernetes"], seniority: "senior", location: "Moscow", salary_max: null, implicit_filters: [...]}
API->>Cache: GET search_hash(intent + explicit_filters)
alt Cache Hit
Cache-->>API: Cached Candidate IDs + Facets
else Cache Miss
API->>OS: Search Request (Hybrid DSL: BM25 + kNN + Filters)
OS-->>API: Top 100 Hits (candidate_ids, _score, highlights, vectors)
API->>LLM: RAG Request (Candidate Profiles + Query -> Generate Summary + Fit Score)
LLM-->>API: Enriched Results [ {id, summary, fit_score, match_reasons, red_flags} ]
API->>Cache: SET search_hash -> Result (TTL 5-15 min)
end
API-->>FE: 200 OK {items: [...], total: 1245, facets: {...}, took_ms: 120}
2. Query Understanding (NLP / Query Parsing) — «Мозг» поиска
Свободный текст рекрутера ("нужен гошник в москву до 300к, чтоб куберы знал") должен превратиться в структурированный SearchIntent.
Архитектура NLP-пайплайна:
- Preprocessing: Нормализация (lowercase, удаление стоп-слов, лемматизация —
pymorphy2/spaCy/razdel). - Entity Extraction (NER):
- Skills/Taxonomy Matching: Словарь таксономии (Aho-Corasick / Trie / RapidFuzz) ->
["Go", "Kubernetes", "Docker"]. - Seniority Detection: Regex / Classifier (
"senior","lead","3+ years","team lead") ->seniority: "senior". - Location Extraction: Geo-coding (DaData / Яндекс.Геокодер / внутренний справочник) ->
location: {city_id: 1, radius_km: 50}. - Salary Parsing: Regex (
"до 300к","300-400k","300тыс") ->salary_max: 300000, currency: "RUB". - Employment Type:
"remote","relocation","part-time".
- Skills/Taxonomy Matching: Словарь таксономии (Aho-Corasick / Trie / RapidFuzz) ->
- Intent Classification: BERT/Linear Head на эмбеддингах запроса ->
role_category: "backend"(для бустов). - Query Expansion (Query Rewriting):
- Синонимы из таксономии:
Go -> Golang. - Связанные скиллы (Co-occurrence / Graph):
Kubernetes -> Docker, Helm, Istio, Prometheus(добавляем какshouldс низким бустом). - Расширение локаций:
Москва -> Москва + МО (опционально).
- Синонимы из таксономии:
Выходная структура (Go DTO):
type SearchIntent struct {
OriginalQuery string
Role string // "Backend Developer"
Seniority SeniorityLevel // Senior
Skills []TaxonomySkill // [{ID: "skill_go", Name: "Go", Weight: 1.0}, {ID: "skill_k8s", Weight: 0.9}]
Location *GeoFilter // {CityID: 1, Lat: 55.75, Lon: 37.61, RadiusKM: 50, IncludeRelocation: true}
SalaryRange *SalaryFilter // {Max: 300000, Currency: "RUB", Net: true}
EmploymentTypes []string // ["full_time", "remote"]
MustHaveSkills []string // Skill IDs с флагом "mandatory" (из фраз "обязательно", "must have")
ExcludeKeywords []string // Negative constraints
Boosts map[string]float64 // Динамические бусты для DSL
}
3. Построение DSL-запроса для OpenSearch (Query Builder)
Генерация запроса — самая ответственная часть. Ошибка в nested фильтре или бусте = нерелевантные результаты.
Ключевые принципы DSL:
nestedдля скиллов и опыта: Скиллы — массив объектов{skill_id, level, years}. Фильтр{"nested": {"path": "skills", "query": {...}}}.- Hybrid Search (BM25 + kNN): Используем
knnсекцию +queryсекцию +rank(RRF) или линейную комбинацию скоров. - RRF (Reciprocal Rank Fusion): Стандарт гибридного поиска в OpenSearch 2.x+. Не требует настройки весов.
Пример генерации DSL на Go (упрощенно):
func BuildSearchDSL(intent SearchIntent, pagination Pagination) map[string]any {
// 1. kNN Query (Semantic Search) - вектор запроса генерируется отдельно (Embedding Service)
knnQuery := map[string]any{
"field": "embeddings.profile_vector",
"query_vector": intent.QueryVector, // []float32
"k": 100, // Кандидатов для реранкинга
"num_candidates": 500,
"filter": buildHardFilters(intent), // Хард-фильтры (локация, зарплата, активность) применяются и к kNN
}
// 2. BM25 Query (Keyword Search) - структурированный bool query
boolQuery := map[string]any{
"must": []map[string]any{
// Текстовый поиск по позиции/описанию с бустами
{"multi_match": map[string]any{
"query": intent.OriginalQuery,
"fields": []string{"position^3", "skills.name^2", "experience.description"},
"type": "best_fields",
"operator": "or",
"fuzziness": "AUTO",
}},
},
"should": []map[string]any{},
"filter": buildHardFilters(intent), // Те же хард-фильтры
"must_not": buildExclusions(intent),
}
// Добавляем бусты для скиллов (should clauses с весами)
for _, s := range intent.Skills {
boost := s.Weight * 2.0 // Базовый буст
if contains(intent.MustHaveSkills, s.ID) {
boost = 10.0 // Must have -> очень высокий буст или перенос в filter
}
boolQuery["should"] = append(boolQuery["should"].([]map[string]any), map[string]any{
"nested": map[string]any{
"path": "skills",
"query": map[string]any{
"term": map[string]any{"skills.skill_id": map[string]any{"value": s.ID, "boost": boost}},
},
"inner_hits": map[string]any{"size": 3, "name": "matched_skills"}, // Для хайлайтов/причин
},
})
}
// 3. Hybrid Search с RRF
searchBody := map[string]any{
"size": pagination.Size,
"from": pagination.From,
"query": map[string]any{
"hybrid": map[string]any{
"queries": []map[string]any{
{"knn": knnQuery},
{"bool": boolQuery},
},
"rank": map[string]any{"rrf": map[string]any{"window_size": 50, "rank_constant": 20}},
},
},
"sort": []map[string]any{
{"_score": "desc"},
{"updated_at": "desc"}, // Тай-брейкер: свежесть профиля
},
"track_total_hits": true,
"highlight": map[string]any{
"fields": map[string]any{
"position": map[string]any{},
"experience.description": map[string]any{},
"skills.name": map[string]any{},
},
"pre_tags": []string{"<mark>"},
"post_tags": []string{"</mark>"},
},
"aggs": buildFacetsAggs(), // Фасеты для UI фильтров
}
return searchBody
}
func buildHardFilters(intent SearchIntent) []map[string]any {
filters := []map[string]any{
{"term": map[string]any{"active_job_search": true}},
}
if intent.Location != nil {
filters = append(filters, map[string]any{
"bool": map[string]any{
"should": []map[string]any{
{"geo_distance": map[string]any{"distance": fmt.Sprintf("%dkm", intent.Location.RadiusKM), "location.geo": map[string]any{"lat": intent.Location.Lat, "lon": intent.Location.Lon}}},
{"term": map[string]any{"location.relocation": true}}, // Готовы к релокации
},
"minimum_should_match": 1,
},
})
}
if intent.SalaryRange != nil && intent.SalaryRange.Max > 0 {
filters = append(filters, map[string]any{
"range": map[string]any{"salary_expectation.amount": map[string]any{"lte": intent.SalaryRange.Max}},
})
}
// Seniority mapping to experience years
if intent.Seniority != "" {
minYears := map[SeniorityLevel]int{"junior": 0, "middle": 2, "senior": 5, "lead": 7}[intent.Seniority]
filters = append(filters, map[string]any{
"nested": map[string]any{
"path": "experience",
"query": map[string]any{
"range": map[string]any{"experience.start_date": map[string]any{"lte": fmt.Sprintf("now-%dy", minYears)}},
},
},
})
}
return filters
}
4. Re-ranking & LLM Enrichment (RAG Pattern)
OpenSearch возвращает Top 50-100 кандидатов. Это «сырой» результат. Далее — LLM Re-ranker / Summarizer.
Зачем нужен LLM на этом этапе:
- Semantic Re-ranking: OpenSearch (BM25/kNN) не понимает контекст «нужен Go, но без микросервисов, только монолит» или «Team Lead, который пишет код 50% времени». LLM понимает.
- AI Summary (Snippet Generation): Вместо обрезанного текста — осмысленное саммари: «Иван: 5 лет Go, последние 2 года ведет команду из 5 человек в Яндексе, опыт Highload (100k RPS), есть опыт внедрения Kubernetes. Зарплата 350к, готов к релокации. Match: 92%».
- Fit Score & Red Flags: Явная оценка соответствия и риски («Зарплата выше рынка», «Нет опыта с Kubernetes, только Docker Swarm»).
Архитектура RAG-запроса:
# Псевдокод Python-сервиса (FastAPI + LangChain / LlamaIndex / нативный клиент)
async def rerank_and_summarize(query: str, candidates: List[CandidateDoc], intent: SearchIntent) -> List[EnrichedCandidate]:
# 1. Подготовка контекста (Compact Representation)
# Не пихаем весь профиль в контекст (лимит токенов), берем фичи
context_items = []
for c in candidates:
context_items.append(f"""
ID: {c.id}
Name: {c.full_name}
Position: {c.position}
Skills: {', '.join([f'{s.name}({s.level})' for s in c.skills[:15]])}
Experience: {'; '.join([f'{e.role} @ {e.company} ({e.start}-{e.end})' for e in c.experience[:3]])}
Location: {c.location.city}, Remote: {c.location.remote}, Relocation: {c.location.relocation}
Salary: {c.salary_expectation.amount} {c.salary_expectation.currency}
Summary: {c.profile_text[:500]}...
""")
prompt = f"""
Ты — экспертный рекрутер. Запрос пользователя: "{query}".
Структурированный интент: {intent.json()}.
Проанализируй {len(candidates)} профилей кандидатов ниже.
Для КАЖДОГО кандидата верни СТРОГО JSON объект:
{{
"candidate_id": "uuid",
"fit_score": 0.0-1.0, // Общая релевантность
"match_reasons": ["Причина 1", "Причина 2"], // Почему подходит (конкретно)
"gaps": ["Пробел 1", "Пробел 2"], // Чего не хватает
"red_flags": ["Риск 1"], // Зарплата, локация, частая смена работы
"summary": "Краткое саммари 2-3 предложения для рекрутера.",
"salary_fit": "match | high | low | unknown"
}}
Кандидаты:
{''.join(context_items)}
"""
# Вызов LLM (Structured Output / Function Calling / JSON Mode)
response = await llm_client.chat.completions.create(
model="gpt-4o-mini", # или локальный Llama 3 70B / Mistral
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}, # или tools
temperature=0.1,
)
parsed = parse_llm_response(response)
# Сортируем по fit_score desc
return sorted(parsed, key=lambda x: x.fit_score, reverse=True)
Оптимизации (Cost/Latency):
- Batch Size: Не более 20-30 кандидатов за вызов (лимит контекста/качество).
- Caching: Кэш эмбеддингов запросов + кэш LLM-ответов по хешу
(query_hash + candidate_ids_hash). - Async: Параллельные вызовы LLM для батчей.
- Fallback: Если LLM упал/таймаут -> возвращаем OpenSearch
_score+ простой шаблонный саммари.
5. Фасетная навигация и Аналитика (Aggregations)
Рекрутеру нужны фильтры слева: «Скиллы», «Локации», «Зарплата», «Источник».
В OpenSearch это aggs на отфильтрованном наборе (post_filter или global).
"aggs": {
"skills": {
"nested": { "path": "skills" },
"aggs": {
"top_skills": { "terms": { "field": "skills.skill_id", "size": 50 } }
}
},
"locations": { "terms": { "field": "location.city", "size": 20 } },
"salary_stats": { "stats": { "field": "salary_expectation.amount" } },
"seniority_dist": {
"scripted_metric": { // Считаем seniority по опыту на лету или используем предвычисленное поле
"init_script": "state.counts = [:]",
"map_script": "def y = doc['experience.start_date'].size()>0 ? (new Date().year - doc['experience.start_date'].value.year) : 0; def level = y<2?'junior':y<5?'middle':y<8?'senior':'lead'; state.counts[level] = (state.counts[level]?:0)+1",
"combine_script": "state.counts.putAll(states.collect{it.counts}.sum())",
"reduce_script": "return states.collect{it.counts}.sum()"
}
},
"sources": { "terms": { "field": "source_profiles.source", "size": 10 } }
}
Важно: Агрегации должны быть быстрыми (кэш в Redis на 1-5 мин по хешу фильтров).
6. Производительность и SLA (Performance)
| Этап | Target Latency (p99) | Оптимизации |
|---|---|---|
| NLP Parsing | < 50 ms | Локальные модели (ONNX Runtime / Go bindings), кэширование таксономии в памяти. |
| OpenSearch Query | < 150 ms | Warmers, правильный маппинг (keyword vs text), index.force_merge для старых сегментов, кэш шардов (index.queries.cache.enabled), правильный routing (по tenant_id). |
| LLM Re-ranking | < 800 ms (async) | Параллельные батчи, стриминг ответов, кэш, fallback. |
| Total API (p99) | < 500 ms (без LLM) / < 1.5s (с LLM) | FE показывает скелетоны, подгружает LLM-часть отдельным запросом (SSE/WebSocket) для лучшего UX. |
Инфраструктура OpenSearch:
- Data Nodes: Hot/Warm/Cold архитектура. Активные резюме (обновлены < 30 дней) — на Hot (NVMe). Архив — на Warm/Cold (HDD/S3).
- Sharding: 1 шард на 10-20M доков. Routing по
tenant_id(SaaS) для изоляции нагрузки. - Replicas: 1 реплика для HA + поисковой нагрузки.
7. Качество поиска (Search Quality / Offline Evaluation)
Senior подход: Метрики качества > Метрики скорости.
- Offline Metrics (на исторических данных / разметке):
- NDCG@10 / MAP@10: По набору тестовых запросов с релевантностями (0/1/2/3).
- Recall@K: Находим ли мы «идеального» кандидата в топ-100.
- Click-through Rate (CTR) / Application Rate: Онлайн метрики.
- A/B Testing Framework:
- Эксперименты с бустами, весами гибридного поиска, промптами LLM.
- Randomization Unit:
session_idилиrecruiter_id.
- Judgment Collection (Human-in-the-loop):
- UI для рекрутеров: «Релевантен / Не релевантен» на карточке.
- Логирование
query_id,candidate_id,position_in_results,action(view, contact, reject).
- Learning to Rank (LTR):
- Обучение модели (LambdaMART / LightGBM / XGBoost) на фичах:
bm25_score,knn_score,skill_overlap,salary_diff,location_match,freshness,source_quality. - Деплой модели как OpenSearch LTR Plugin (SltrQuery) для нативного реранкинга до LLM.
- Обучение модели (LambdaMART / LightGBM / XGBoost) на фичах:
Резюме (TL;DR для архитектора)
> «Поиск — это не просто match запрос. Это конвейер Query Understanding -> Hybrid Retrieval (BM25 + kNN + Hard Filters) -> LTR Re-rank -> LLM Rerank/Summarize.
>
> Ключевые решения в коде:
> 1. NLP Pipeline (Go/Python): Извлечение сущностей (NER) по таксономии + Query Expansion. Выход — типизированный SearchIntent.
> 2. OpenSearch DSL Builder: Строгая типизация, nested запросы для скиллов/опыта, Hybrid Search с RRF, хард-фильтры (geo, salary, seniority) на уровне filter context (кэшируются).
> 3. Two-Stage Ranking: OpenSearch (Recall 100-200) -> LLM Re-ranker (Precision Top 20). LLM дает fit_score, summary, gaps, red_flags.
> 4. Observability & Quality: Логирование полного трейса запроса, офлайн NDCG/MAP, A/B фреймворк для бустов/промптов, сбор разметки рекрутерами.
> 5. Performance: Кэширование эмбеддингов/запросов в Redis, Hot/Warm ноды OS, асинхронная LLM-часть для UX.
>
> Это дает рекрутеру «Google-like» опыт: вводит естественный язык, получает релевантный топ-10 с понятными саммари и причинами соответствия».
Вопрос 35. Какие LLM-модели используются для AI-фич?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:32:57"/>
Ответ собеседника: правильный. 90% запросов — GPT-4o Mini, 10% — свои дообученные модели.
Правильный ответ:
Выбор моделей — это компромисс между Quality (качество), Latency (задержка), Cost (стоимость), Data Privacy (безопасность данных) и Vendor Lock-in. Ответ кандидата отражает реальную продакшн-стратегию: General Purpose API для большинства задач + Specialized Self-Hosted для критичных/специфических. На уровне Tech Lead нужно понимать, как это организовано архитектурно, как происходит роутинг, оценка качества и фоллбэки.
1. Стратегия «Model Garden» (Портфель моделей)
Мы не завязываемся на одну модель. Есть Registry моделей с метаданными, через который проходят все запросы.
| Категория | Модели (Примеры 2024) | Use Cases | Доля трафика | SLA Latency |
|---|---|---|---|---|
| General Purpose (API) | GPT-4o Mini (основная), GPT-4o (сложные), Claude 3.5 Sonnet (fallback/benchmark) | Генерация саммари, рерайтинг вакансий, чат-помощник, извлечение сущностей (NER) из запроса, классификация интента. | ~85-90% | p99 < 2-3s (streaming first token < 500ms) |
| Specialized Self-Hosted (Own) | Llama 3.1 8B / 70B (QLoRA/Full FT), Mistral-Nemo 12B, Qwen 2.5 7B/14B | Reranking кандидатов (Fit Score), Парсинг резюме (PII-sensitive), Извлечение скиллов/таксономия, Генерация эмбеддингов (если не BERT). | ~10-15% | p99 < 500-800ms (батчинг, KV-cache) |
| Embedding Models | BGE-M3 (multi-lingual), E5-Mistral-7B, text-embedding-3-small (API) | Векторный поиск (kNN), кластеризация, семантический дедуп. | 100% (индексация) | Batch: высокая пропускная способность |
| Guardrails / Safety | Llama Guard 3, Nemo Guardrails, кастомный BERT-классификатор | Проверка промптов на инъекции, PII, токсичность, соответствие политике. | 100% (pre/post) | < 50ms |
2. Архитектура LLM Gateway (Unified Access Layer)
Все вызовы LLM проходят через внутренний шлюз (LLM Gateway) на Go. Прямых вызовов OpenAI/Anthropic из бизнес-логики нет.
flowchart LR
subgraph Client [Clients: Search API, Parser, Chatbot]
C1[Search Service]
C2[Resume Parser]
C3[HR Chatbot]
end
GW[LLM Gateway (Go Service)]
subgraph Routing [Smart Router]
R1[Task Classifier<br/>Intent + Complexity]
R2[Model Selector<br/>Policy Engine]
R3[Feature Flags / A/B]
end
subgraph Providers [Providers Pool]
P1[OpenAI / Azure OpenAI<br/>GPT-4o, 4o-mini]
P2[Anthropic / Vertex AI<br/>Claude 3.5 Sonnet]
P3[Self-Hosted vLLM / TGI<br/>Llama 3.1 70B FT, 8B FT]
P4[Local Embeddings<br/>BGE-M3 / E5]
end
subgraph Observability [Observability]
O1[Langfuse / Helicone / Custom ClickHouse]
O2[Cost Tracking per Feature/Model]
O3[Quality Eval Pipeline]
end
Client --> GW
GW --> Routing
Routing --> Providers
Providers --> GW
GW --> Observability
Ключевые компоненты Gateway:
- Request Normalification: Единый интерфейс
ChatCompletionRequest(OpenAI-совместимый) для всех провайдеров. - Smart Routing (Policy Engine):
type RoutingPolicy struct {TaskType string // "rerank", "summarize", "extract_skills", "chat"Complexity string // "low", "medium", "high" (оценка по длине контекста/сложности инструкций)PrivacyLevel string // "public", "internal", "pii", "secret"PreferredModels []ModelID // ["gpt-4o-mini", "llama-3.1-70b-ft-rerank"]FallbackChain []ModelID // ["claude-3.5-sonnet", "gpt-4o"]MaxLatencyMs intMaxCostPer1k float64}
- Пример:
Task=rerank, Privacy=pii-> Только Self-Hosted (Llama 70B FT). - Пример:
Task=summarize, Privacy=public, Complexity=low-> GPT-4o Mini. - Пример:
Task=chat, Complexity=high-> GPT-4o / Claude 3.5 Sonnet.
- Пример:
- Retry & Fallback: Экспоненциальный бэкоф, переключение на фоллбэк-модель при 5xx/429/timeout. Важно: сохранять контекст диалога при фоллбэке.
- Streaming Proxy: Проброс SSE (
text/event-stream) клиенту без буферизации всего ответа (TTFT — Time To First Token). - Budget Enforcement: Хард-лимиты по токенам/деньгам на фичу/пользователя/день. Возврат
429 Quota Exceededс понятной ошибкой.
3. Self-Hosted Inference Stack (Свои модели)
Почему свои? PII (152-ФЗ), Cost at scale, Latency control, Custom Logic (Logits/Constraints).
Инфраструктура:
- Сервер инференса: vLLM (PagedAttention, Continuous Batching, Prefix Caching) — стандарт де-факто для высокой пропускной способности. Альтернатива: TGI (Text Generation Inference) от HF.
- GPU: H100 / A100 (80GB) для 70B (FP8/INT4 квантизация), L4 / A10G для 8B-14B.
- Оркестрация: Kubernetes (KServe / Ray Serve / custom Operator) + KubeRay для масштабирования реплик по нагрузке (HPA по
gpu_utilization+queue_length). - Мониторинг: DCGM Exporter (GPU metrics), vLLM
/metrics(queue size, token throughput, cache hit rate).
Fine-Tuning Pipeline (MLOps):
- Data Curation: Сбор логов
(prompt, completion, human_feedback)-> Deduplication -> Quality Filter (LLM-as-a-Judge) -> Train/Val/Test split. - Method: QLoRA (4-bit NF4 + LoRA r=64/128) для 70B / Full SFT для 8B. PEFT позволяет дообучать 70B на 2-4x H100 за часы.
- Base Models:
meta-llama/Meta-Llama-3.1-70B-Instruct,mistralai/Mistral-Nemo-Base-12B,Qwen/Qwen2.5-14B-Instruct. - Evaluation (CI/CD для моделей):
- Offline: Benchmark на тестовом сете (Accuracy, F1, Rouge, JSON Schema Validity, Style Match).
- Online: Shadow Deployment (зеркалим 5% трафика на новую модель, сравниваем метрики
accept_rate,edit_distance,latency). - Regression Tests: Набор «золотых» промптов, где ответ не должен меняться (или меняться предсказуемо).
- Deployment: Blue/Green или Canary в K8s. Роллбэк за 1 минуту (переключение Service/Ingress).
Пример: Модель для Re-ranking (Fit Score)
- Base: Llama 3.1 70B Instruct.
- Data: 50k примеров
(Job_Description + Candidate_Profile -> Structured JSON: fit_score, gaps, summary). - Technique: QLoRA (r=128, alpha=256) + Constrained Decoding (Outlines / Guidance / JSON Schema enforcement) — гарантированно валидный JSON без ретраев.
- Latency: ~300-500ms на кандидата (батч 8-16 шт) на 2x H100.
- Cost: ~0.002-0.005 у GPT-4o Mini) при нагрузке > 10k req/day.
4. Работа с Провайдерами API (OpenAI / Anthropic / Azure)
- Azure OpenAI (Приоритет для Enterprise): Гарантии data residency (Европа/США), SLA 99.9%, управление ключами через Azure AD / Key Vault, приватные эндпоинты (Private Link). Используем для продакшн-трафика GPT-4o/4o-mini.
- Direct OpenAI / Anthropic: Для R&D, бенчмарков, фич, где нужен самый свежий релиз модели раньше, чем в Azure.
- Cost Optimization:
- Prompt Caching: Включен по умолчанию (OpenAI, Anthropic, Gemini). Кэшируем системные промпты, таксономию, few-shot примеры.
- Batch API (OpenAI): Для асинхронных задач (переиндексация, ночная обработка) — 50% скидка.
- Model Distillation: Логируем
(input, output)от GPT-4o -> обучаем свою маленькую модель (8B) для конкретной задачи (Distillation).
5. Observability & Quality Assurance (LLMOps)
Без этого «черный ящик» не управляем.
| Метрика | Инструмент | Alerting |
|---|---|---|
| Latency (TTFT, TPOT, E2E) | Gateway Metrics (Prometheus/Grafana) | p99 > SLA |
| Error Rate / 429 / 5xx | Gateway Logs (Loki/ELK) | Spike > 1% |
| Cost (USD / 1k tokens / feature) | ClickHouse / DataDog (Cost Attribution) | Daily spend > Budget * 1.2 |
| Token Usage (Prompt/Completion) | Gateway Metrics | Anomaly detection |
| Quality (Offline Eval) | Langfuse / Weights & Biases / Custom | NDCG/F1 drop on Golden Set |
| Quality (Online) | User Feedback (👍/👎), Implicit (Accept/Edit/Regenerate) | Acceptance Rate < Threshold |
| Hallucination / Schema Violations | Guardrails / Output Validator | Rate > 0.5% |
| PII Leakage (Output) | Presidio / Regex Scanner on Gateway Output | Critical Alert (PagerDuty) |
Evaluation Pipeline (Непрерывная):
- Golden Dataset: 500-1000 кейсов на каждую задачу (парсинг, саммари, реранк) с эталонными ответами.
- LLM-as-a-Judge (GPT-4o / Claude 3.5): Сравнивает ответ новой модели с эталоном / ответом продакшн-модели. Критерии:
Accuracy,Completeness,Tone,Format. - CI Gate: Мерж модели в реестр только если
Judge_Score >= Production_Score - Delta.
6. Безопасность и Комплаенс (Guardrails)
Слой защиты на Gateway (Input & Output):
// Псевдокод мидлвари Gateway
func GuardrailsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
req := ParseRequest(r)
// 1. INPUT GUARDS
if DetectPromptInjection(req.Messages) { // Heuristics + Llama Guard / DeBERTa classifier
return Error(w, "Prompt injection detected", 400)
}
if DetectPII(req.Messages) && req.PrivacyLevel == "public" { // Presidio / Regex
return Error(w, "PII in prompt not allowed for this model", 400)
}
if EstimateTokens(req) > MaxContextWindow(req.Model) {
return Error(w, "Context length exceeded", 400)
}
// 2. CALL MODEL
resp := next.ServeHTTP(w, r) // Вызов провайдера
// 3. OUTPUT GUARDS (для стриминга - сложнее, нужен буфер или пост-обработка)
if req.RequireJSON {
if !ValidateJSONSchema(resp.Content, req.JSONSchema) {
// Пытаемся починить (Repair) или ретраим с строгим промптом
return RetryOrError(w, "Invalid JSON schema")
}
}
if DetectPII(resp.Content) && !req.AllowPIIOutput {
return Error(w, "PII leakage in response", 500) // Критично!
}
if DetectToxicity(resp.Content) {
return Error(w, "Unsafe content generated", 500)
}
})
}
Резюме (TL;DR для архитектора)
> «Мы используем Multi-Model Gateway как единую точку входа. > > Стратегия: > 1. 90% — GPT-4o Mini (Azure OpenAI): Генерация текста, чат, простые NLP-задачи. Быстро, дешево, качественно, Enterprise-compliant. > 2. 10% — Self-Hosted (vLLM + Llama 3.1 70B/8B QLoRA): Ранжирование (Re-ranking), Парсинг ПДн (PII), Извлечение таксономии. Контроль данных, детерминизм (Constrained Decoding), экономия на масштабе. > 3. Embeddings: BGE-M3 (Self-hosted) для поиска, text-embedding-3-small (API) для фоллбэка. > > Инженерная культура: > * Gateway на Go: Роутинг, фоллбэки, бюджеты, стриминг, гардрейлы (PII, Injection, JSON Schema). > * MLOps: Датасети из логов -> QLoRA -> Offline Eval (LLM Judge) -> Shadow Deploy -> Canary -> Prod. > * Observability: Полная трассировка запроса (TraceID), стоимость на фичу, качество онлайн/оффлайн, алерты на утечки PII и галлюцинации. > * Vendor Independence: Абстракция над провайдерами позволяет сменить OpenAI на Claude или свою модель за 1 деплой конфига роутинга. > > Это дает гибкость: запускаем фичу на API за день, мигрируем на свою модель за неделю, когда набирается датасет и понятна экономика».
Вопрос 36. Какие задачи планируются на ближайшее будущее? (демо в Figma)
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:33:18"/>
Ответ собеседника: правильный. Основная задача — «Шортлисты» (избранное): добавление кандидатов в списки через сердечко или попап, просмотр шортлистов, сравнение кандидатов. Меньшие задачи: генерация синонимов профессий через LLM для расширения поиска. Стратегия: массовые операции в шортлистах → авто-написание в Telegram → умный бот для общения → авто-назначение собеседований.
Правильный ответ:
Это классический переход от Search & Discovery (поиск) к Recruitment CRM / ATS-lite (управление воронкой). На уровне Tech Lead важно не просто «сделать фичи», а заложить архитектуру под State Machine воронки, Event-Driven интеграции с мессенджерами/email/календарями и Human-in-the-loop AI (агенты с подтверждением).
1. Эпик 1: «Шортлисты» (Shortlists / Candidate Lists) — Основа CRM
Это не просто «избранное», а рабочие пространства рекрутера для коллаборации и управления этапами.
Доменная модель (DDD):
// Агрегат
type Shortlist struct {
ID UUID
OwnerID UserID // Рекрутер / Хайринг-менеджер
VacancyID *VacancyID // Опционально: привязка к вакансии
Name string // "Senior Go — Собеседования"
Status ShortlistStatus // Active, Archived, Completed
Columns []KanbanColumn // Кастомные этапы: New -> Screen -> Tech -> Offer -> Hired
Members []Member // Коллаборация: Viewer, Editor, Owner
Candidates []ShortlistItem // Позиции в шортлисте
CreatedAt time.Time
UpdatedAt time.Time
}
type ShortlistItem struct {
CandidateID UUID
ColumnID string // Текущий этап (Kanban)
Order int // Порядок внутри колонки (Drag & Drop)
AddedBy UserID
AddedAt time.Time
Source SourceType // Search, Referral, Import, Bot
Tags []string // "Strong Tech", "Relocation OK"
Notes string // Приватные заметки рекрутера
// AI Enrichment (кэш)
AISummary string // Саммари под эту вакансию
AIFitScore float32 // 0.0 - 1.0
AIRedFlags []string
}
// События (Event Sourcing / Outbox Pattern)
type ShortlistEvent interface {
EventType() string // CandidateAdded, CandidateMoved, ColumnCreated, NoteAdded, Shared, Exported
}
Ключевые фичи MVP (Figma Demo):
- Multi-select & Bulk Actions: Чекбоксы в таблице/карточках -> Toolbar: «Добавить в шортлист», «Переместить в этап», «Написать в Telegram», «Экспорт в Excel/PDF».
- Kanban Board (Drag & Drop): Колонки настраиваемые. Оптимистичное UI (Optimistic Updates) + WebSocket синхронизация коллаборации (Operational Transformation / CRDT для порядка карточек).
- Split View (Сравнение): Выбор 2-5 кандидатов -> Модалка/Страница: таблица «Skill Matrix» (строки — скиллы/критерии, колонки — кандидаты), зарплаты, локации, AI-саммари, ссылки на профили. Рендеринг через AG Grid или TanStack Table с виртуализацией.
- Sharing & Permissions: Публичная ссылка (Read-only) для ХМ (Hiring Manager) без логина (JWT токен с TTL, scope
shortlist:read:{id}).
Технические вызовы:
- Real-time: WebSocket сервер (Go + Centrifugo / Gorilla WS / Socket.io) для синхронизации порядка карточек, комментариев, онлайн-статусов рекрутеров.
- Performance: Индексы в PG:
(shortlist_id, column_id, "order"). Массовые обновленияorderчерезCASE WHENили временную таблицу за 1 запрос. - Search Integration: Шортлист как Saved Search (Dynamic List) — автопополнение по правилам (например, «Все Senior Go в Москве с Fit > 0.8»).
2. Эпик 2: LLM для Таксономии — Query Expansion & Synonyms
Проблема: Рекрутер ищет «Java Developer», а кандидат написал «J2EE», «Spring Boot», «Backend Java». OpenSearch BM25 не найдет без синонимов.
Архитектура пайплайна генерации синонимов:
graph LR
A[Taxonomy Service<br/>Source of Truth] --> B{Trigger: New Role /<br/>Low Recall Metric}
B --> C[LLM Job (Async)]
C --> D[Prompt: Role + Skills +<br/>Market Context +<br/>Existing Synonyms]
D --> E[LLM (GPT-4o Mini /<br/>Llama 3.1 70B FT)]
E --> F[Structured Output:<br/>{canonical: [], synonyms: [],<br/>related_skills: [], stop_words: []}]
F --> G[Human Review UI<br/>(Taxonomist / Lead Recruiter)]
G --> H{Approved?}
H -- Yes --> I[Write to PG +<br/>Invalidate OS Cache]
H -- No --> J[Feedback -> Retrain/Few-shot]
I --> K[OpenSearch Analyzer<br/>Reload (Synonym Graph)]
Детали реализации:
- OpenSearch Synonym Graph Token Filter: Используем
synonym_graphна индексеskillsиposition. Позволяет искать «Go» -> «Golang» + «Golang» -> «Go» корректно сmulti_match. - Embedding-based Expansion (Alternative): kNN поиск в пространстве эмбеддингов скиллов (
skill2vec/BGE-M3). Ближайшие соседи = кандидаты на синонимы. Хорошо для «Long Tail» скиллов. - Versioning: Таксономия версионируется (
taxonomy_v12). Переиндексация OS делается Blue/Green (новый индексcandidates_v13-> алиасcandidatesатомарно переключается).
3. Стратегия: Автоматизация Воронки (Agentic Workflow)
Вектор: Shortlist Bulk Actions → Telegram/Email Bot → AI Conversational Agent → Auto-Scheduling.
Уровень 1: Массовые операции (Bulk Actions) — Текущий квест
- UI: Выбор 50 кандидатов -> «Написать в Telegram».
- Backend: Очередь задач (Redis Streams / Kafka / Asynq).
- Worker (Go): Итерация по списку -> Рендер шаблона (Go
text/template+ i18n) -> Вызов Telegram Bot API (sendMessage/sendMediaGroup). - Rate Limiting: Токен-бакет на уровне бота (30 msg/sec глобально, 1 msg/sec на чат) + ретраи с бэкофом.
- Tracking: UTM-метки в ссылках (
utm_source=shortlist_bulk,utm_campaign=vac_123), пиксели открытия (tracking pixel в HTML-версии), сохранениеMessageIDдля редактирования/удаления. - Compliance: Чек-лист «Согласие на обработку ПДн» перед отправкой (GDPR / 152-ФЗ). Opt-out ссылка в каждом сообщении.
Уровень 2: Telegram Bot / Web App (Mini App) — Интерактив
- Архитектура: Webhook (не Long Polling) -> API Gateway -> Internal Services.
- State Machine: Кандидат в боте имеет состояние:
New -> Contacted -> Replied -> Screening -> Interview_Scheduled -> Rejected/Hired. - Mini App (React/Telegram Web App): Полноценный UI внутри Telegram: профиль вакансии, тестовое задание, календарь слотов, загрузка портфолио.
- Deep Linking:
tg://resolve?domain=bot&start=vac_123_cand_456— вход в контекст конкретной вакансии/кандидата.
Уровень 3: AI Conversational Agent (Screening Bot) — «Умный бот»
- Цель: Провести первичный скрининг (Hard Skills, Soft Skills, Expectations) без рекрутера.
- Стек: LangGraph / LlamaIndex Workflows / Custom Go Orchestrator.
- Паттерн: ReAct (Reasoning + Acting) с Human-in-the-loop для критических решений.
- Промпт-инжиниринг:
- System Prompt: Роль, Tone of Voice, Жесткие ограничения (не обещать зарплату > X, не дискриминировать).
- Tools (Function Calling):
get_vacancy_details(vacancy_id)check_skill_match(candidate_skills, required_skills)-> вызов Search/Embedding API.propose_slots(calendar_ids, timezone)-> интеграция с Calendly/Google Calendar/Outlook.save_screening_report(report_json)-> запись в Shortlist Item / ATS.escalate_to_human(reason)-> уведомление рекрутера в Slack/Telegram/Email.
- RAG для контекста: Векторный поиск по базе знаний компании (FAQ, Benefits, Tech Stack details) для ответов на вопросы кандидата.
- Оценка качества (Evals): Авто-оценка диалогов (LLM-as-a-Judge):
Completeness,Hallucination Rate,Candidate Satisfaction (CSAT),Conversion to Interview.
Уровень 4: Auto-Scheduling (Календари) — Финал воронки
- Интеграция: Cal.com / Calendly API / Nylas / Cronofy / Google Calendar API / Microsoft Graph.
- Логика:
- Рекрутер / ХМ настраивает Availability Rules (понедельник 10-14, среда 15-18, таймзона).
- Бот/Агент предлагает слоты кандидату (кнопки в Telegram / ссылка на календарь).
- Кандидат выбирает -> Атомарное бронирование (Optimistic Locking на слоте в БД календаря).
- Создание Event: Title
[Screen] Ivan Petrov - Senior Go, Guests:[Recruiter, Hiring Manager, Candidate], Description: ссылка на профиль, вопросы для собеседования, Meet/Zoom ссылка (авто-создание через API). - Напоминания: 24h, 1h, 15min (Email / Telegram / Push).
- Синхронизация статуса:
Interview_Scheduled-> в Shortlist / ATS.
4. Нефункциональные требования (NFR) к новой архитектуре
| Требование | Решение |
|---|---|
| Audit Log / Compliance | Event Sourcing для Shortlist / Candidate Journey. Каждое действие (клик, сообщение, смена этапа) = Event в Kafka/ClickHouse. Неизменяемость. |
| Idempotency | Все внешние вызовы (Telegram, Calendar, Email) идемпотентны по Idempotency-Key (hash: action+entity_id+params). |
| Observability | OpenTelemetry трассировка через все сервисы: API -> Gateway -> Worker -> Telegram -> DB. Корреляция по TraceID в логах. |
| Feature Flags | LaunchDarkly / Unleash / Custom для постепенного роллаута: Bot Enabled, AI Agent Enabled, Auto-Schedule Enabled. Kill Switch на случай галлюцинаций бота. |
| Multi-tenancy (SaaS) | Row Level Security (RLS) в PG / Namespace в Kafka / Tenant ID в всех очередях. Изоляция данных ботов и календарей. |
| Scalability | Stateless Workers (Go) + Horizontal Pod Autoscaler (K8s) по длине очереди (KEDA). vLLM для AI Agent масштабируется отдельно. |
Резюме (TL;DR для архитектора)
> «Роумап — это построение Recruitment Operating System поверх поиска. > > 1. Core (Shortlists): DDD-агрегат с Kanban, Real-time коллаборацией (WS), Bulk Actions, Export/Sharing. Фундамент для всего остального. > 2. Intelligence (Taxonomy LLM): Автоматизация Query Expansion через LLM + Human-in-the-loop ревью -> Synonym Graph в OpenSearch. Повышает Recall без потери Precision. > 3. Automation Pipeline (Agentic): > * Phase 1: Bulk Messaging (Telegram/Email) через надежные очереди (Asynq/Kafka), трекинг, комплаенс. > * Phase 2: Telegram Mini App + Bot State Machine для сбора данных. > * Phase 3: AI Screening Agent (LangGraph/LlamaIndex) с Function Calling (Calendar, Search, Save Report, Escalate). RAG на базе знаний. Оценка через LLM-as-a-Judge. > * Phase 4: Auto-Scheduling (Cal.com/Nylas/Graph API) с атомарным бронированием и синхронизацией в Shortlist. > > Архитектурные принципы: Event-Driven (Outbox), Idempotency, Observability (OTel), Feature Flags, Multi-tenancy. Переход от «CRUD над кандидатами» к «Оркестрации бизнес-процессов найма».»
Вопрос 37. Проект приносит прибыль? Каковы риски и планы по монетизации?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:37:19"/>
Ответ собеседника: правильный. Пока не прибылен, есть небольшая выручка. Два рычага: 1) это не основной бизнес, есть другой проект (Friend Job для кандидатов), который покрывает расходы; 2) планы по масштабированию и монетизации рекрутерской части.
Правильный ответ:
На уровне Tech Lead / Engineering Manager разговор о деньгах — это разговор о Unit Economics, Runway, CAC/LTV и техническом долге как финансовом риске. Ответ кандидата честен, но поверхностен. Ниже — фреймворк, как это нужно держать в голове и презентовать стейкхолдерам.
1. Текущее финансовое состояние (Snapshot)
| Метрика | Статус | Комментарий |
|---|---|---|
| Revenue (MRR/ARR) | > 0, но << OpEx | «Небольшая выручка» = пилотные контракты / Early Adopters. Нет Product-Market Fit (PMF) в монетизации. |
| Profitability | Negative (Burn Rate > Revenue) | Ожидаемо для B2B SaaS на стадии Scale-up / Growth. |
| Funding Source | Cross-subsidization (Friend Job) | Риск Single Point of Failure: Если Friend Job теряет трафик/монетизацию — у рекрутерского продукта 0 Runway. |
| Runway (своими силами) | 0 месяцев | Зависимость от внешнего кэшфлоу. |
Вывод: Это не «бизнес», а внутренний стартап (Intrapreneurship) или R&D инициатива, финансируемая кэш-кой Friend Job. Стратегия — выйти на самодостаточность.
2. Unit Economics — язык бизнеса для инженера
Мы должны знать цифры на каждую фичу и пользователя.
Расчеты (Target / Current):
| Метрика | Формула | Target (PMF) | Current Reality | Action Items (Tech) |
|---|---|---|---|---|
| CAC (Customer Acquisition Cost) | Marketing + Sales Salaries / New Customers | < 3-5 месяц ARPU | Высокий (ручные демо, длинный Sales Cycle) | PLG (Product Led Growth): Self-service Signup, Free Tier, In-app Upgrade, Public API Docs. |
| LTV (Lifetime Value) | ARPU * Gross Margin % / Churn Rate | > 3x CAC | Низкий (Churn высокий, нет апсейлов) | Retention Features: Daily Digest, AI Insights, Integrations (ATS, Calendar), Switching Costs. |
| Gross Margin | (Revenue - Infra Cost - Support) / Revenue | > 75-80% (SaaS Standard) | Критично низкий из-за LLM Costs | Cost Optimization: Routing, Caching, Distillation, Self-hosted Models (см. Q35). |
| Churn Rate (Logo / Revenue) | Lost Customers / Total | < 5% Monthly (Logo) / Negative Net Revenue Retention | Высокий (пилоты не переходят в прод) | Onboarding Automation: Time-to-Value < 1 день. Feature Adoption Tracking (Amplitude/Mixpanel). |
| Infra Cost per Active Recruiter | Total Cloud/GPU Bill / MAU | < $10-20 / month | $50-100+ (GPU, API Calls) | Multi-tenancy Optimization: Shared GPU pools, Cold Start optimization, Batch Processing. |
Технологический долг как финансовый риск:
- Монолит / Отсутствие Multi-tenancy -> Невозможно масштабировать продажи (Onboarding нового клиента = ручная настройка инфраструктуры, дни работы DevOps). Fix: Kubernetes Namespaces / Terraform Modules / Feature Flags для изоляции тенантов.
- Vendor Lock-in (OpenAI) -> Риск роста цен на 10x или отключения API. Fix: Gateway + Self-hosted Fallback (уже в работе).
3. Стратегия Монетизации: От «Фич» к «Пакетом»
Нельзя продавать «поиск» — продают «решение проблемы найма».
Модель ценообразования (Packaging):
| План | Target Audience | Price Point (Monthly/Seat) | Key Value Metrics (Entitlements) | Technical Enforcement |
|---|---|---|---|---|
| Free / Community | Indie Recruiters, SMB | $0 | 1 Active Vacancy, 50 Profiles View, 10 AI Credits, Community Support | Hard Limits in DB (Quotas), Rate Limiting (Gateway). |
| Pro | In-house Recruiters, Agencies (Small) | $150-300 | Unlimited Vacancies, 5000 Profiles, AI Screening Bot, Telegram Bot, Shortlists (Unlimited), Export, Email Support | Feature Flags (ai_screening, telegram_bot), Quota Counters (Redis/DB). |
| Enterprise | Corp HR, Large Agencies, RPO | $500-1000+ (Custom) | SSO (SAML/OIDC), RBAC (Custom Roles), Audit Logs (SIEM), SLA 99.9%, Dedicated Infra Option, Custom Integrations (ATS/HRIS), Data Residency (EU/US), Contract & Invoice | Multi-tenancy Hard Isolation (Separate K8s Namespaces/Clusters), Audit Log Pipeline (Kafka -> ClickHouse/SIEM), Terraform for Dedicated Deploy. |
Usage-based Component (AI Credits):
- Базовый пакет включает N кредитов.
- Перерасход = $0.XX / 1k tokens (Screening, Parsing, Embeddings).
- Зачем: Перекладывает риск LLM Cost на клиента, стимулирует эффективные промпты.
4. План выхода на безубыточность (Roadmap to Break-even)
Phase 1: Product-Market Fit & PLG (Q1-Q2) — «Сделать покупаемым самим»
- Tech: Self-Service Onboarding (Signup -> Vacancy -> First Candidate in 15 min).
- Tech: Free Tier с жесткими квотами (включаем LLM для «Вуу-эффекта»).
- Biz: In-app Billing (Stripe/Paddle) + Usage Metering.
- Goal: 50+ Paying Pro Customers, MRR $15k+, CAC Payback < 6 months.
Phase 2: Enterprise Readiness & Expansion (Q3-Q4) — «Продавать корпам»
- Tech: SSO (SAML/OIDC), SCIM Provisioning, Audit Logs (Immutable), RBAC, Data Export/Delete (GDPR/152-FZ).
- Tech: Private Link / VPC Peering / Dedicated Deploy (BYOC) для банков/госкорпораций.
- Tech: Нативные интеграции: Huntflow, Potok, Lever, Greenhouse, BambooHR (Webhooks + Sync Engine).
- Biz: Outbound Sales + Partner Program (Agencies).
- Goal: 5-10 Enterprise Logos, ACV $20k+, Net Revenue Retention > 110%.
Phase 3: Platform & Ecosystem (Year 2+) — «Монополия на внимании»
- Marketplace: Платные плагины (Налоговый калькулятор, Рело케이ция, Assessment Providers).
- Data Product: Анонимизированные бенчмарки зарплат / скиллов (Subscription для HR-аналитиков).
- White-label: Продукт как «Career Site Engine» для крупных работодателей.
5. Главные Риски (Risk Register) и Митигация
| Риск | Вероятность | Влияние | Митигация (Technical & Business) |
|---|---|---|---|
| 1. Зависимость от Friend Job (Funding Risk) | High | Critical (Shutdown) | Deadline: Break-even Plan к концу Q2. Tech: Cost Cutting (GPU -> CPU/Quantization), Feature Freeze non-core. |
| 2. LLM Cost Explosion / API Price Hike | Medium | High | Gateway Strategy (Routing, Caching, Self-hosted Llama 3.1 70B/8B). Contracts: Enterprise Agreement с Azure/OpenAI. |
| 3. Нет PMF в монетизации (Churn) | High | High | Discovery: Jobs-to-be-Done entrevью с ушедшими клиентами. Metrics: DAU/MAU, Feature Adoption, NPS. Tech: Kill unused features, упростить Core Loop. |
| 4. Конкуренция: LinkedIn Recruiter / Huntflow / ATS с AI | High | Medium | Differentiation: «AI-First Sourcing» (не CRM, а поиск + скрининг). Niche: Tech Hiring (Go, K8s, ML) — вертикализация таксономии. Speed: Time-to-First-Hire метрика. |
| 5. Регуляторика (152-ФЗ, GDPR, AI Act) | Medium | High (Fines / Ban) | Privacy by Design: PII Masking в Gateway, Data Residency (EU/РФ кластеры), DPIA для AI Screening, Human-in-the-loop обязателен. |
| 6. Техдолг блокирует Enterprise Sales | High | Medium | Refactoring Sprint: Multi-tenancy, Audit Logs, SSO — как Enabler Features (блокеры сделок), а не «техзадолженность». |
| 7. Key Person Risk (ML/Infra Expert) | Medium | Medium | Documentation: ADR (Architecture Decision Records), Runbooks, Cross-training, Bus Factor > 1 для GPU Stack. |
6. Что я (Tech Lead) делаю прямо сейчас для бизнеса
- Cost Governance: Еженедельный дашборд
Cost per Active User / Cost per 1k AI Requestsв Grafana. Алерт если > порога. - Entitlements Engine: Пишу централизованный сервис
Entitlements(Go + OPA/Rego), который решает: «Может ли этот пользователь запустить AI Screening?». Это разблокирует Billing и Packaging без хардкодаif plan == "pro"в 50 местах. - Multi-tenancy Foundation: Миграция на Row Level Security (RLS) в Postgres + Namespace-based K8s Deployment. Цель: новый Enterprise клиент поднимается Terraform за 30 мин, а не 2 недели.
- Observability для Biz: Инструментирую ключевые воронки (Signup -> First Search -> First Shortlist -> First Message -> Paid) в OpenTelemetry/Amplitude.
- AI Eval Pipeline: Непрерывная оценка качества моделей (Offline + Online), чтобы не потерять качество при миграции на дешевые Self-hosted модели.
Резюме (TL;DR для CEO/Investors)
> «Продукт сейчас Pre-Revenue / Early Revenue, финансируется кэшфлоу от Friend Job. Это дает нам время (Runway), но создает стратегическую хрупкость. > > План на 12 месяцев: > 1. Q1-Q2: Запуск Self-Service Pro Plan (20k MRR, покрытие Infra Costs (Gross Margin > 0%). Ключ — Entitlements Service и PLG Onboarding. > 2. Q3-Q4: Enterprise Pack (SSO, Audit, SLA, Dedicated Deploy, ATS Integrations). Цель: 3-5 логотипов по 30k+ ACV. Ключ — **Multi-tenancy Hardening** и **Compliance (152-ФЗ/GDPR)**. > 3. **Параллельно:** Агрессивная оптимизация **LLM Costs** (Self-hosted Llama 3.1, Routing, Caching) — цель снизить Cost per Screening с 0.50 до $0.02. > > Главный технический блокер для продаж Enterprise: Отсутствие SSO/Audit/Multi-tenancy. Это приоритет #1 для инженерии. > Главный бизнес-риск: Не найдем PMF в монетизации до окончания кэшфлоу Friend Job. Митигация — жесткие дедлайны по MRR и еженедельный ревью Unit Economics.»
Вопрос 38. Какая зарплатная вилка предусмотрена для этой вакансии?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:39:14"/>
Ответ собеседника: правильный. 110–140 тысяч рублей на руки (после вычета налогов).
Правильный ответ:
Ответ кандидата содержит конкретные цифры (Net), что хорошо для переговоров, но с позиции Senior/Tech Lead требует перевода в Gross (до вычета НДФЛ), понимания уровня позиции и рыночного контекста. Ниже — разбор того, как правильно оценивать, озвучивать и негоциировать компенсацию на собеседовании.
1. Перевод Net в Gross (Базовая финансовая грамотность)
В России работадатель платит НДФЛ 13% (для налоговых резидентов) и страховые взносы ~30% (на себя, сверху). Кандидат называет Net (на руки). Компания оперирует Gross (в контракте) и Total Cost (Full Cost to Company).
Расчет для диапазона 110–140к Net:
| Метрика | Формула | Низ (110к) | Верх (140к) |
|---|---|---|---|
| Gross (в оффере/контракте) | Net / 0.87 | ~126 437 ₽ | ~160 920 ₽ |
| Total Cost (с взносами ~30%) | Gross * 1.3 | ~164 368 ₽ | ~209 196 ₽ |
> Важно: На собеседовании всегда оперируйте Gross (сумма в оффере). Назвать Net можно только в уточняющем вопросе: "Это примерно 110-140к на руки, верно?".
2. Привязка к Уровню (Grade) и Стеку (Go)
Вилка 126k–161k Gross (Москва/Remote РФ, 2024-2025) соответствует Middle+ / Senior уровню. Для Staff/Principal/Tech Lead ожидания обычно стартуют от 200k–250k+ Gross (230k–290k+ Net).
Ориентиры рынка (Gross, Москва/Remote, коммерческая разработка):
| Грейд | Go Experience | Gross Range (в месяц) | Net Range (на руки) | Комментарий |
|---|---|---|---|---|
| Strong Middle | 3-5 лет | 180k – 240k | 156k – 208k | Автономная работа над фичами, менторинг стажеров. |
| Senior | 5-7+ лет | 220k – 320k | 191k – 278k | Архитектура, код-ревью, хайлоад, найм, тех. стратегия. |
| Tech Lead / Team Lead | 6-8+ лет | 280k – 400k+ | 243k – 348k+ | Управление командой (3-8 чел.), доставка, P&L ответственность. |
| Staff / Principal | 8-10+ лет | 350k – 550k+ | 304k – 478k+ | Кросс-командная архитектура, R&D, влияние на бизнес. |
Вывод по вилке кандидата (126-161k Gross):
- Это нижняя граница Senior или верхняя граница Middle+.
- Если вакансия под Senior/Lead — кандидат занижает планку (или не знает рынка).
- Если вакансия под Middle — это топовая вилка, оправданная только сильным хардом (K8s, Highload, Distributed Systems).
3. Структура Компенсации (Total Rewards)
На уровне Senior+ обсуждают не только Base Salary. «Правильный» ответ должен учитывать пакет:
type CompensationPackage struct {
BaseSalaryGross int64 // Фикс (Gross), ежемесячно
BonusStructure Bonus // Переменная часть
Equity *Equity // Опционы/RSU (для продуктовых/стартапов)
Benefits Benefits
ReviewCycle string // "Раз в 6 мес. / Раз в год"
IndexationPolicy string // "По инфляции / По рынку / По производительности"
}
type Bonus struct {
Type string // "KPI / OKR / Company Performance / Spot"
TargetPct float64 // От Jahresgehalt (напр. 15-25% для Senior)
MaxPct float64 // Cap (напр. 200% от Target)
PayoutFreq string // "Quarterly / Semi-annually / Annually"
}
type Equity struct {
Type string // "ISO / NSO / RSU / Phantom Shares"
Vesting string // "4 years, 1 year cliff, monthly/quarterly"
CliffMonths int
StrikePrice *float64 // Для опционов
CurrentFMV *float64 // Fair Market Value (для оценки стоимости)
}
type Benefits struct {
HealthInsurance string // "ДМС (включая стоматологию/психо) для себя + семья"
EducationBudget int64 // "150-300k / год на курсы, конфиренции, книги"
HardwareBudget int64 // "MacBook Pro M3 Max / Linux Workstation + периферия"
VacationDays int // "28 кл. + доп. оплачиваемые (напр. 5-10 дней)"
SickLeavePolicy string // "Полная оплата больничных без лимита дней"
RemotePolicy string // "Full Remote / Hybrid / Relocation Support"
CoWorkingBudget int64 // "Оплата коворкинга / интернета"
}
Чек-лист для кандидата (что уточнить перед соглашением на оффер):
- Бонусы: Как считаются? Зависит ли от меня лично или только от компании? Есть ли гарантированный минимум?
- Ревью: Как часто? Кто инициирует? Есть ли автоматическая индексация за инфляцией?
- ДМС: Покрывает ли семью? Есть ли стоматология, психолог, телемедицина?
- Обучение: Есть ли бюджет на конфиренции (GopherCon, HighLoad++, KubeCon)? Оплачивается ли время на обучение?
- Железо: Можно ли выбрать Linux/Windows? Есть ли бюджет на мониторы, кресло, док-станцию дома?
- Опционы (если есть): % пула, strike price, liquidation preferences, возможность продать на вторичке (tender offer).
4. Тактика Переговоров (Negotiation Framework)
Не называйте первое число (если можно). Заставьте работодателя озвучить бюджет позиции (Salary Band).
Сценарий А: Работодатель спрашивает ожидания (как у вас)
> Слабый ответ: «Хочу 140к на руки». (Якоряетесь на нижней границе, не знаете Gross, не оставили место для маневра). > > Сильный ответ (Senior/Lead): > > «Исходя из моего опыта (Go, Highload, Team Lead), сложности задач, которые мы обсуждали, и текущего рынка, я рассматриваю офферы в диапазоне 220–280k Gross (Base) + бонусная программа + опционы/бенефиты. > > Для меня важно не только базовая вилка, но и структура ревью, политика индексации и equity-компонента. Каков бюджет позиции (Salary Band) и как выглядит пакет бенефитов?»
Почему это работает:
- Называете Gross (язык бизнеса).
- Даете вилку (гибкость), а не точку.
- Обосновываете контекстом (опыт, задачи, рынок).
- Сразу переводите разговор в Total Rewards (бонусы, equity, ревью).
- Задаете вопрос обратно (получаете информацию).
Сценарий Б: Оффер ниже ожиданий (Lowball)
> «Спасибо за оффер. Base 160k Gross ниже моих ожиданий (маркет 220k+ для этого скоупа). > Готов обсудить, если есть сильный Variable (Target 30%+), Sign-on Bonus (на закрытие текущих опционов/бонусов у прошлого работодателя) или ускоренный Vesting / Review через 3-6 месяцев с привязкой к метрикам. Как вы смотрите на такой компромисс?»
5. Специфика Go-разработчика (Market Premium)
Go-разработчики в 2024-2025 гг. имеют премию 10-20% над Python/Java/Node.js на уровне Senior+ из-за:
- Дефицита кадров (меньше джунов, сложнее переквалифицироваться).
- Критичности нагрузки (Highload, Microservices, K8s, Cloud Native — основные домены Go).
- Стоимости ошибки (инфраструктура, платежи, real-time).
Аргументы для зарплатного запроса (Go-specific):
- «Пишу production-ready код на Go 5+ лет, в том числе CGO, unsafe, профилирование (pprof, trace), оптимизация GC под низкую латентность (p99 < 50ms).»
- «Опыт проектирования распределенных систем: gRPC, Kafka/NATS, Consul/Etcd, Service Mesh (Istio/Linkerd), Observability (OTel, VictoriaMetrics, Loki).»
- «Внедрял Platform Engineering практики: Internal Developer Platform на Kubernetes (Operators, CRD, Controller-runtime), CI/CD (ArgoCD, GitLab), GitOps.»
- «Менторил команду 5+ человек, проводил Code Review, собеседования, писал RFC/ADR.»
6. Пример SQL для HR/Finance: Анализ вилок по грейдам
Если вы строите компенсационную политику внутри компании:
-- Актуальные вилки (Salary Bands) по грейдам для Go-инженеров (Москва/Remote RF, 2024 H2)
-- Источники: hh.ru, Habr Career, Levels.fyi, Glassdoor, опросы сообществ (GoWay, Golang Moscow)
WITH market_data AS (
SELECT
'Middle+' AS grade, 180000 AS p25_gross, 220000 AS p50_gross, 260000 AS p75_gross UNION ALL
SELECT
'Senior', 230000, 280000, 330000 UNION ALL
SELECT
'Senior+', 280000, 330000, 380000 UNION ALL
SELECT
'Tech Lead', 300000, 360000, 420000 UNION ALL
SELECT
'Staff Eng', 350000, 420000, 500000
),
-- Внутренние офферы за последний квартал
internal_offers AS (
SELECT
grade,
AVG(base_gross) AS avg_offer_gross,
COUNT(*) as offers_count
FROM job_offers
WHERE created_at >= NOW() - INTERVAL '3 months'
AND stack ILIKE '%go%'
AND status IN ('accepted', 'rejected_salary')
GROUP BY grade
)
SELECT
m.grade,
m.p50_gross AS market_median,
m.p25_gross AS market_low,
m.p75_gross AS market_high,
i.avg_offer_gross AS our_avg_offer,
i.offers_count,
CASE
WHEN i.avg_offer_gross < m.p25_gross THEN '🔴 Below Market (Hiring Risk)'
WHEN i.avg_offer_gross < m.p50_gross THEN '🟡 Below Median (Hard to Close Seniors)'
WHEN i.avg_offer_gross > m.p75_gross THEN '🟢 Above Market (Attractive)'
ELSE '🟢 Competitive'
END AS competitiveness_status
FROM market_data m
LEFT JOIN internal_offers i ON m.grade = i.grade
ORDER BY
CASE m.grade
WHEN 'Middle+' THEN 1
WHEN 'Senior' THEN 2
WHEN 'Senior+' THEN 3
WHEN 'Tech Lead' THEN 4
WHEN 'Staff Eng' THEN 5
END;
Резюме (TL;DR для кандидата)
> Ваш ответ «110–140к Net» = ~126–161k Gross. > > 1. Всегда говорите в Gross (цифра в контракте/оффере). > 2. Знайте свой грейд: Эта вилка — Middle+ / Junior Senior. Если претендуете на Senior/Lead — занижаете. > 3. Рыночная норма Senior Go (2024): 220–320k Gross (Base) + Bonus (15-25%) + Benefits + Equity. > 4. Негодируйте пакетом: Base + Bonus Target/Cap + Review Cycle + Equity + DMS/Обучение/Железо/Отпуска. > 5. Тактика: Не называйте первым число. Спросите Salary Band. Обоснуйте запрос стеком (Highload, K8s, Arch, Team Lead) и маркетом.
Вопрос 38. Рассматривается ли зарплата 180 тысяч на руки?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:39:44"/>
Ответ собеседника: правильный. Основатель ответил, что нужно взять паузу и проверить финансовую модель на ближайшие месяцы, так как зарплата заложена в бюджете продукта.
Правильный ответ:
Этот момент — классический переговорный триггер. Кандидат поднял ставку с 140к до 180к Net (+28%), а основатель не сказал «нет», а ушел в «проверку финансовой модели». Это сигнал: деньги есть, но их нужно обосновать инвесторам/собственникам или перераспределить бюджет. Ниже — разбор того, что происходит «под капотом» у основателя, как правильно вести себя кандидату в паузе и как подготовить аргументацию для финального решения.
1. Математика предложения: 180к Net = Сколько это для компании?
Основатель считает не в Net, а в Total Cost of Employment (TCE) — полная стоимость сотрудника для бизнеса.
Расчет TCE для 180 000 ₽ Net (на руки):
| Статья расходов | Формула / Ставка | Сумма (₽) | Комментарий |
|---|---|---|---|
| Net (на руки) | — | 180 000 | То, что просит кандидат. |
| НДФЛ (13%) | Net / 0.87 * 0.13 | 26 897 | Удержка из Gross. |
| Gross (в оффере/контракте) | Net / 0.87 | 206 897 | База для взносов. |
| Страховые взносы (30.2%) | Gross * 0.302 | 62 483 | Пенсионный, ФОМС, ФСС — платит работодатель сверху. |
| Итого TCE (Full Cost) | Gross + Взносы | 269 380 | Реальная цена 1 FTE в месяц. |
| TCE в год | TCE * 12 | 3 232 560 | ~$35k USD/год (по курсу 90). |
Сравнение с предыдущей вилкой (140к Net):
- 140к Net -> TCE ~ 209 000 ₽/мес.
- 180к Net -> TCE ~ 269 000 ₽/мес.
- Дельта: +60 000 ₽/мес (+29%) нагрузки на бюджет продукта.
> Почему основатель ушел в «финансовую модель»: Ему нужно понять, укладывается ли продукт в Runway (срок жизни денег) с этой нагрузкой. Если Burn Rate вырастет на 60к/мес, Runway сократится на N месяцев. Ему нужно пересчитать: CAC, LTV, Break-even point, Cash Flow Gap.
2. Что происходит в «Паузе» (Financial Model Check)
Основатель (или CFO/FinOps) прямо сейчас считает сценарии в Excel/Google Sheets/BI. Кандидат должен понимать эти сценарии, чтобы аргументированно ответить после паузы.
Сценарий А: Бюджет «Hard Capped» (Жесткий лимит)
- Условие: Инвестор выделил фиксированную сумму на Payroll (Fond Opytov Truda - ФОТ) на квартал/год. Превышение требует подписания аддендума к инвестиционному соглашению или_board approval_.
- Действие основателя: Проверяет, есть ли «воздух» в текущем ФОТ. Может ли он урезать расходы на другое (маркетинг, инфраструктура, аутсорс), чтобы покрыть +60к.
- Риск для кандидата: Если «воздуха» нет — ответ будет «Максимум 150-160к Net».
Сценарий Б: Unit Economics Sensitivity (Чувствительность юнит-экономики)
- Условие: Продукт пред-доходный (Pre-revenue) или ранний MRR. Зарплата — главная статья OpEx.
- Расчет:
New Runway = Cash Balance / (Current Burn + 60 000). - Порог: Если Runway падает ниже 6-9 месяцев — это красный флаг для инвесторов. Основатель не может себе этого позволить без планов по привлечению следующего раунда.
- Аргумент кандидата: «Мое приходе сократит Time-to-Market на X месяцев / принесет первый Enterprise контракт / снизит Infra Costs на Y%, что компенсирует мою дельту зарплаты».
Сценарий В: Internal Equity (Внутренняя справедливость)
- Условие: В команде уже есть Senior/Lead за 140-150к Net.
- Проблема: Если дать 180к новичку, старые сотрудники узнают (всегда узнают) -> демотивация / уход / требование пересмотра.
- Действие основателя: Проверяет bandas (грейды). Может ли он оформить 180к как «Senior+ / Staff Engineer» с другим скоупом ответственности (Tech Lead без людей, Architect, IC-Track), чтобы обосновать разницу грейдом, а не именем.
3. Стратегия кандидата во время паузы и после
Не делайте:
- ❌ Пишете каждые 30 минут: «Ну как там решение?».
- ❌ Сразу соглашаетесь на 150к «чтобы не терять оффер» (проигрыш 360к/год).
- ❌ Угрожаете уходом: «Если не 180к — ухожу» (до этапа оффера это слабая позиция).
Делаете (Senior/Tech Lead Approach):
Шаг 1. Фиксация контекста (Сразу после ответа основателя) > «Понял, Алексей/Иван. Это правильный подход — считать юнит-экономику. Давайте договоримся: я жду вашего обратной связи до [Дата/Время, напр. завтра 15:00 / через 2 дня]. > Пока вы считаете модель, я подготовил краткую памятку по тому, как я планирую окупить эту дельту за первые 3-6 месяцев (Impact Map). Скину вам в Telegram/Email, чтобы у вас были цифры для разговора с инвесторами/партнерами, если понадобится».
Шаг 2. Подготовка «Impact Map» (One-pager для основателя) Это ваш главный инструмент влияния. Пришлите ДО финального звонка.
Структура Impact Map (Markdown/PDF):
# Обоснование компенсации 180k Net (TCE ~270k): ROI за 6 месяцев
## 1. Прямая экономия Infra Costs (Hard Savings)
- **Текущая проблема:** LLM API bills $2k+/мес, Cold Starts GPU.
- **Мое решение (Weeks 1-4):** Миграция на Self-hosted Llama 3.1 70B (vLLM/TGI) + Semantic Caching (Redis/Valkey).
- **Ожидаемый эффект:** -70% LLM Costs = **~$1,400/мес (~126k ₽/мес) экономии**.
- **ROI:** Дельта зарплаты (60k) окупается **только за счет инфры за 1 месяц**.
## 2. Ускорение Time-to-Revenue (Revenue Enablement)
- **Блокер продаж Enterprise:** Нет SSO (SAML/OIDC), Audit Logs, RBAC, Multi-tenancy.
- **Мой опыт:** Внедрял это в [Проект X] за 6 недель командой из 2х. Сам пишу код (IC), не только менеджу.
- **План:** MVP Enterprise Pack за 4-6 недель.
- **Потенциал:** 1 Enterprise Deal (ACV $20k+) = **~1.8M ₽ ARR**. Комиссия/бонус + покрытие моей зарплаты на год.
## 3. Снижение Bus Factor & Hiring Costs (Risk Mitigation)
- **Риск:** Key Person Risk (только один знает GPU/ML стек).
- **Действие:** Документирую архитектуру (ADR), выношу Runbooks, обучаю команду (Knowledge Sharing).
- **Экономия:** Избегаем найма второго ML-инженера (TCE +300k/мес) на 6-12 месяцев.
## 4. Технологический долг как финансовый риск
- **Проблема:** Монолит блокирует онбординг клиентов (2 недели DevOps на тенанта).
- **Решение:** Multi-tenancy (K8s Namespaces + Postgres RLS + Terraform) -> Онбординг за 30 мин.
- **Эффект:** Sales Cycle -30%, CAC снижение, масштабируемость без линейного роста команды DevOps.
## Итоговая таблица ROI
| Метрика | Текущее | Через 3 мес (Мой вклад) | Финансовый эффект |
| :--- | :--- | :--- | :--- |
| **Infra Cost / MAU** | $50 | $15 | **-$35 * MAU / мес** |
| **Enterprise Readiness** | 0% | 80% (SSO, Audit, MT) | **Unlocks $50k+ Pipeline** |
| **Bus Factor (Core Infra)** | 1 | 3 | **Risk Mitigation** |
| **Onboarding New Tenant** | 2 недели | 30 мин | **Sales Velocity x3** |
**Вывод:** Моя дельта к зарплате (+60k/мес к TCE) — это не *Cost*, а *Investment* с Payback Period < 1.5 месяцев и IRR > 300%.
4. Варианты развития событий (Game Theory)
После паузы основатель вернется с одним из ответов. Ваша реакция:
Вариант 1: «Ок, 180к Net согласовали» (Win)
- Действие: Просите оффер-леттер (Offer Letter) с указанием Gross (206 897 ₽), структуры бонусов, ревью, бенефитов.
- Важно: Уточните: «В оффере Gross 206 897 ₽? Бонусная схема отдельно? Ревью через 6 мес с привязкой к метрикам выше?».
Вариант 2: «Максимум 160к Net (184к Gross / TCE 240к)» (Compromise)
- Контекст: Основатель готов пойти навстречу, но хардкэп бюджета не позволяет 180к.
- Контр-предложение (Sign-on / Accelerated Review): > «Готов принять 160к Net (Gross ~184к) как Base Salary при двух условиях: > 1. Sign-on Bonus (разовый) в размере 240к ₽ (компенсация дельты за 4 месяца + покрытие форы опционов/бонусов у текущего работодателя). > 2. Гарантированный Salary Review через 3 месяца (не 6, не год) до 180к Net при выполнении KPI: Enterprise Pack в проде + Infra Cost < $20/MAU. > Это снимает риск с бюджета сейчас (Base 160к) и дает мне мотивацию дать результат быстро».
Вариант 3: «Бюджет только 140к Net, выше нельзя» (Hard No / Lowball)
- Диагностика: Либо проект очень беден (Runway < 3 мес), либо основатель не видит в вас Senior/Lead ценности, либо Internal Equity блокирует.
- Реакция: > «Понял. 140к Net — это уровень Strong Middle / Junior Senior на рынке сейчас (Gross ~161к). Для скоупа, который мы обсуждали (Arch Ownership + Team Lead + GPU/ML + Enterprise Delivery), маркет 200-250к Gross. > Если бюджет жестко зафиксирован на 140к, я не могу принять оффер, так как это создаст диссонанс с ответственностью и рисками. > Давайте останемся в контакте: если появится бюджет на Senior+ грейд или откроется роль Tech Lead — пишите, с радостью обсудим».
- Почему так: Уважение к себе и рынку. Работа за заниженную зарплату с высокой ответственностью -> быстрый выгорание и уход через 3-6 мес. Основателю это дороже.
5. SQL для основателя: Моделирование Runway с новой зарплатой
Если вы (как Tech Lead) хотите помочь основателю «продать» эту зарплату инвесторам/партнерам, скиньте ему этот скрипт для его финансовой модели.
-- Параметры сценария
WITH params AS (
SELECT
269380::numeric AS new_tce_monthly, -- TCE для 180k Net
209000::numeric AS old_tce_monthly, -- TCE для 140k Net
5000000::numeric AS current_cash, -- Текущие деньги на счете (пример)
1200000::numeric AS current_burn_monthly,-- Текущий Burn Rate (без этой зарплаты)
150000::numeric AS expected_infra_saving_monthly, -- Ожидаемая экономия инфры (мои действия)
0::numeric AS expected_revenue_monthly -- Консервативно: 0 до Enterprise запуска
),
scenarios AS (
SELECT
'Current (140k Net)' AS scenario,
current_burn_monthly + old_tce_monthly AS total_burn,
current_cash / (current_burn_monthly + old_tce_monthly) AS runway_months
FROM params
UNION ALL
SELECT
'Proposed (180k Net) - Pessimistic' AS scenario,
current_burn_monthly + new_tce_monthly AS total_burn,
current_cash / (current_burn_monthly + new_tce_monthly) AS runway_months
FROM params
UNION ALL
SELECT
'Proposed (180k Net) - Realistic (Infra Savings)' AS scenario,
current_burn_monthly + new_tce_monthly - expected_infra_saving_monthly AS total_burn,
current_cash / (current_burn_monthly + new_tce_monthly - expected_infra_saving_monthly) AS runway_months
FROM params
UNION ALL
SELECT
'Proposed (180k Net) - Optimistic (Infra + 1 Enterprise Deal M3)' AS scenario,
-- M1-M3: с экономией инфры. M4+: +Revenue
-- Упрощенно: средний бёрн за 6 мес
(current_burn_monthly + new_tce_monthly - expected_infra_saving_monthly) * 3
+ (current_burn_monthly + new_tce_monthly - expected_infra_saving_monthly - 300000) * 3 -- предполагаем revenue 300k/net profit
/ 6.0 AS avg_burn_6m,
current_cash / (
(current_burn_monthly + new_tce_monthly - expected_infra_saving_monthly) * 3
+ (current_burn_monthly + new_tce_monthly - expected_infra_saving_monthly - 300000) * 3
) / 6.0 AS runway_months
FROM params
)
SELECT
scenario,
total_burn AS burn_rate_monthly,
ROUND(runway_months, 1) AS runway_months,
CASE
WHEN runway_months < 6 THEN '🔴 Critical'
WHEN runway_months < 9 THEN '🟡 Risk Zone'
WHEN runway_months < 12 THEN '🟢 Safe'
ELSE '🟢 Comfortable'
END AS risk_level
FROM scenarios;
Результат этого запроса (пример):
| scenario | burn_rate_monthly | runway_months | risk_level |
|---|---|---|---|
| Current (140k Net) | 1,409,000 | 3.5 | 🔴 Critical |
| Proposed (180k Net) - Pessimistic | 1,469,380 | 3.4 | 🔴 Critical |
| Proposed (180k Net) - Realistic (Infra Savings) | 1,319,380 | 3.8 | 🟡 Risk Zone |
| Proposed (180k Net) - Optimistic | ~1,000,000 | 5.0 | 🟢 Safe |
> Инсайт для кандидата: Даже в пессимистичном сценарии разница в Runway всего ~3-4 дня (3.5 vs 3.4 мес). В реалистичном (с вашей экономией инфры) — Runway растет. Это мощнейший аргумент: «Моя зарплата не сокращает Runway, она его увеличивает за счет оптимизаций, которые только я могу сделать».
Резюме (TL;DR)
- 180к Net = 270к TCE/мес. Дельта +60к к бюджету.
- Пауза основателя = пересчет Runway и Internal Equity. Это нормально, это процесс.
- Ваша задача в паузе: Прислать Impact Map (ROI калькулятор: экономия инфры, разблок Enterprise, Bus Factor).
- Аргумент «Киллер»: «Моя дельта зарплаты окупается за 1 месяц только за счет переноса LLM на Self-hosted + кэширование».
- Готовьтесь к компромиссу: Base 160к Net + Sign-on 240к + Review через 3 мес до 180к.
- Red Line: Если хардкэп 140к Net без бонусов/ревью/опционов — уходите. Это не Senior/Lead позиция, это эксплуатация.
Вопрос 39. Почему вы ушли с прошлого места работы и ищете новую?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:40:33"/>
Ответ собеседника: правильный. Уволился недавно (18-е число). Причина — стагнация: не было интересных задач, хочется решать более сложные проблемы для профессионального роста.
Правильный ответ:
Ответ кандидата честен и понятен, но с позиции Senior/Tech Lead он слишком пассивен. Фраза «хочется решать более сложные проблемы» звучит как запрос потребностей, а не как предложение ценности. На уровне Senior+ уход из-за «стагнации» требует контекста: почему не удалось создать сложные задачи внутри текущей роли? (Tech Lead часто создает сложность/архитектуру, а не ждет её). Ниже — как перевести этот ответ из плоскости «я хочу» в плоскость «я приношу/я решил».
1. Структура идеального ответа на «Почему ушли?» (Framework: Context → Action → Result → Next Step)
Хороший ответ на этот вопрос не оправдывается, а продвигает вашу кандидатуру. Он должен закрыть страхи нанимающего менеджера:
- Flight Risk (не уйдете ли через год по той же причине?).
- Conflict/Toxicity (не конфликтный ли вы?).
- Competence (не уволили ли за некомпетентность под прикрытием «стагнации»?).
- Motivation Alignment (совпадает ли наш стек/скоуп с тем, чего вам не хватало?).
Шаблон Senior-ответа (STAR-адаптация): > Ситуация (Context): «В предыдущей компании я работал [N] лет, прошел путь от [Role] до [Senior/Lead]. За последние [период] продукт перешел в фазу [Maintenance / Stable Growth / Legacy Support].» > > Проблема (Pain Point): «Мой сфокус ответственности был [описание]. Бизнес приоритизировал [feature factory / bugfixing / cost cutting], а архитектурные инициативы (например, миграция на Kubernetes, выделение ML-платформы, рефакторинг монолита в сервисы) блокировались или деприоритизировались.» > > Действие (Your Agency — КЛЮЧЕВОЙ МОМЕНТ): «Я не просто ждал. Пытался инициировать: писал RFC/ADR на [тему], проводил PoC для [технологии], менторил джуниоров, чтобы делегировать рутину и высвободить время на стратегию. Результат: [успех/частичный успех/блокер от бизнеса].» > > Причина ухода (Decision): «Понял, что мой текущий уровень экспертизы [Go, Highload, Distributed Systems, Team Lead] и аппетит к сложности (Technical Depth / Scope) не находят применения в текущих бизнес-целях компании. Оставаться означало бы деградацию навыков (Skill Rot).» > > Почему МЫ (Next Step): «Именно поэтому ваша вакансия зацепила: вы решаете [проблема из JD: Highload / ML Infra / Platform Engineering / Greenfield], стек [Go, K8s, Kafka...], иscope — [Tech Lead / IC / Architect]. Я вижу прямое попадание моего бэкграунда в ваши вызовы: [конкретный пример: опыт оптимизации GC под p99 < 50ms / внедрение GitOps / построение IDP].»
2. Разбор ошибок кандидата и как их исправить
| Ошибка в ответе кандидата | Почему это слабо для Senior/Lead | Как усилить (Rephrase) |
|---|---|---|
| «Причина — стагнация» | Звучит пассивно. Стагнация — это состояние среды. Senior — это агент изменений. | «Компания вошла в фазу эксплуатации (Maintenance Mode), где ROI от архитектурных улучшений ниже, чем от быстрых фич. Мой скиллсет (Architecture, Highload, Platform) перестал приносить бизнес-ценность там». |
| «Не было интересных задач» | «Интересные» — субъективно. Бизнес платит за сложные/ценные. | «Задачи сместились в сторону Feature Factory / Bug Fixing / Legacy Support. Задач уровня Staff/Principal (System Design, Cross-team initiatives, R&D, Hiring bar raising) в бэклоге не было». |
| «Хочется решать более сложные проблемы» | Запрос «дайте мне сложности». Работодатель ищет «решаю сложности». | «Ищу роль, где мой опыт в [Domain] будет применен к задачам класса [Complexity Level: Scale, Latency, Consistency, Team Enablement], чтобы закрывать боли бизнеса: Cost Reduction, Time-to-Market, Reliability». |
| «Для профессионального роста» | Эгоистично. Компания не школа. | «Чтобы масштабировать свой импакт: не просто писать код, а строить платформы/процессы/команды, которые умножают производительность остальных». |
3. Чек-лист «Красные флаги» для интервьюера (и как их не поднять)
Интервьюер мысленно проверяет эти пункты. Ваш ответ должен явно или неявно их закрывать:
- Performance Management (PIP/Underperformance)?
- Закрытие: «Прошел последний Performance Review на уровне [Exceeds Expectations / Top 10%], получил повышение/бонус. Увольнение по собственному желанию, notice period отработал полностью, передача дел подписана [Team Lead / Manager].»
- Conflict / Toxic Culture?
- Закрытие: «Отношения с командой и менеджментом хорошие, остаемся в контакте. Ухожу исключительно по техническому/карьерному вектору (Tech Vector), а не People Vector.»
- Job Hopper (3 работы за 2 года)?
- Закрытие: Если так — объясняйте макро-контекст (Startup shutdown, Acquisition, Relocation, Contract end). Если ушли с последнего места через 3 месяца — красный флаг. Готовьте железное обоснование: «Ожидался Greenfield/Architecture, на деле — Legacy Support/Outsourcing management. Не соответствие JD реальности».
- Overqualified / Bored again in 6 months?
- Закрытие: Покажите Long-term Vision: «Вижу себя здесь 3-5+ лет. Интересна траектория: Senior -> Staff -> Principal / Tech Lead -> Engineering Manager внутри этой продуктовой линейки».
4. Примеры сильных ответов под разные контексты ухода
Кейс А: Стартап закрылся / Пайплайн кончился (Самый безопасный)
> «Стартап не поднял следующий раунд (Series A fell through), команда расформирована. Ищу стабильный продуктовый команд с длинным горизонтом планирования, где можно построить архитектуру "на века", а не "на демо инвесторам за 2 недели". Ваш профиль [B2B SaaS / Fintech / Highload] идеально попадает.»
Кейс Б: Большая корпорация / Банк — бюрократия, Legacy, медленно (Частый кейс)
> «Работал в [Big Corp/Bank] над [Core System]. Процессы: 3 месяца на approve дизайна, 2 недели на деплой, Change Advisory Board на каждый пуш. Технологически застряли в [Java 8 / Monolith / On-prem K8s 1.18]. Я тянул инициативу по модернизации (Strangler Fig, GitOps), но организационная инерция не позволяла двигаться быстрее. Хочу в среду с High Engineering Culture: CI/CD < 10 min, RFC Culture, Blameless Postmortems, где инженер решает "как", а не согласовывает "можно ли".»
Кейс В: Аутсорс / Аутстафф — нет ownership, контекст переключается (Частый кейс)
> «Был в аутсорсе/аутстаффе. Минус: нет Product Ownership — передаешь проект и забываешь. Плюс: видел 5-7 разных архитектур/стеков/команд за 2 года. Хочу уйти в In-house Product, чтобы нести ответственность за долгосрочные последствия своих архитектурных решений (Technical Debt Ownership), развивать платформу годами и растить команду.»
Кейс Г: "Стагнация" в продуктовой компании (Контекст кандидата — самый рискованный)
> Структура ответа (прямо в лоб, честно, но проактивно): > 1. Факт: «Был Senior/Tech Lead в продуктовой команде [N] человек. Проект в стабильной фазе, MAU растет органически, большие рефакторинги сделаны (перешли на Go 1.22, K8s 1.28, вынесли Payments в отдельный сервис).» > 2. Попытка изменить: «Инициировал RFC на [ML-based Anomaly Detection / Platform Team creation / Multi-region Active-Active]. Получил обратную связь от CTO/VP: "Сейчас фокус на Unit Economics / New Market Launch / Cost Cutting, инновационный бюджет заморожен на 6-12 мес".» > 3. Вывод: «Я уважаю бизнес-решение, но моя экспертиза — в построении сложных распределенных систем / платформ / высоконагруженных путей. Сидеть в Maintenance Mode и ждать года — значит терять Hard Skills (GC Tuning, eBPF, Consensus Protocols, K8s Operators). Я ушел, чтобы продолжить расти технически там, где это нужно бизнесу прямо сейчас.» > 4. Мост к вам: «Ваша задача [из JD: "построить IDP / оптимизировать LLM Inference / разобраться с шардированием PG"] — это именно тот класс проблем, где я даю максимальный ROI уже из коробки.»
5. Go-код: Генератор безопасного ответа (для подготовки)
package interview
import "fmt"
type ResignationReason struct {
PreviousRole string // "Senior Go Engineer / Tech Lead"
CompanyStage string // "Series B Fintech / Enterprise Bank / Outsource"
TenureMonths int
BusinessContext string // "Maintenance Mode / Cost Cutting / Pivot / Shutdown"
TechStack []string // "Go, K8s, Kafka, ClickHouse"
InitiativesTried []string // "RFC: Migration to gRPC, PoC: eBPF profiling, Hiring Bar Raising"
BlockerReason string // "Biz Priority / Budget Freeze / Org Structure / Tech Debt Freeze"
TargetRoleScope string // "Staff Engineer / Tech Lead / Platform Lead"
TargetChallenges []string // "Highload, System Design, Team Enablement, Greenfield"
}
func (r ResignationReason) GenerateSeniorAnswer() string {
return fmt.Sprintf(`
**Контекст:** %s в %s (%d мес.). Стек: %v.
**Бизнес-ситуация:** Компания в фазе "%s". Приоритеты сместились на %s.
**Моя активность (Agency):** Пытался двигать иглу: %v.
**Блокер:** %s. Понимаю бизнес-обоснование, но это создает несоответствие (Misalignment) между моим грейдом (%s) и доступным скоупом задач.
**Решение:** Ушел по собственному желанию, отработал notice period, передача дел подписана. Нет конфликтов, PIP или performance issues.
**Почему Я ЗДЕСЬ:** Ищу роль %s, где скоуп включает: %v.
Мой опыт (%v) напрямую закрывает ваши боли: [ссылка на JD: Highload / Cost / Scale / Team].
`, r.PreviousRole, r.CompanyStage, r.TenureMonths, r.TechStack,
r.BusinessContext, r.BlockerReason, r.InitiativesTried, r.BlockerReason, r.PreviousRole,
r.TargetRoleScope, r.TargetChallenges, r.TechStack)
}
func main() {
reason := ResignationReason{
PreviousRole: "Tech Lead (Go)",
CompanyStage: "Series B Fintech (Stable Growth)",
TenureMonths: 28,
BusinessContext: "Maintenance Mode / Cost Optimization",
TechStack: []string{"Go 1.22", "K8s 1.28", "Kafka", "ClickHouse", "Postgres"},
InitiativesTried: []string{"RFC: Multi-region Active-Active", "PoC: Self-hosted LLM Inference (vLLM)", "Initiative: Platform Team Spin-up"},
BlockerReason: "Budget Freeze on Innovation, Focus only on Feature Factory",
TargetRoleScope: "Staff Engineer / Tech Lead (Platform/Infra)",
TargetChallenges: []string{"Highload (100k RPS)", "Platform Engineering (IDP)", "ML Infra Optimization", "Team Enablement"},
}
fmt.Println(reason.GenerateSeniorAnswer())
}
6. SQL для HR: Проверка кандидата на «Flight Risk» по истории
-- Анализ паттерна смены работы (Job Hopping / Stagnation Pattern)
WITH work_history AS (
SELECT
candidate_id,
company_name,
role_title,
start_date,
end_date,
EXTRACT(EPOCH FROM (end_date - start_date))/86400/30 AS tenure_months,
resignation_reason_category, -- 'Stagnation', 'Layoff', 'Better Offer', 'Relocation', 'Conflict', 'Startup Shutdown'
performance_rating_last -- 'Exceeds', 'Meets', 'PIP'
FROM candidate_experience
WHERE candidate_id = $1
),
patterns AS (
SELECT
candidate_id,
COUNT(*) AS total_jobs,
AVG(tenure_months) AS avg_tenure_months,
MIN(tenure_months) AS min_tenure_months,
SUM(CASE WHEN resignation_reason_category = 'Stagnation' THEN 1 ELSE 0 END) AS stagnation_count,
SUM(CASE WHEN tenure_months < 12 THEN 1 ELSE 0 END) AS short_stints_lt_1yr,
SUM(CASE WHEN performance_rating_last = 'PIP' THEN 1 ELSE 0 END) AS pip_flags,
MAX(CASE WHEN resignation_reason_category = 'Conflict' THEN 1 ELSE 0 END) AS has_conflict_flag
FROM work_history
GROUP BY candidate_id
)
SELECT
candidate_id,
avg_tenure_months,
stagnation_count,
short_stints_lt_1yr,
CASE
WHEN pip_flags > 0 THEN '🔴 HIGH RISK: Performance Issues History'
WHEN has_conflict_flag = 1 THEN '🟡 MEDIUM RISK: Conflict History'
WHEN stagnation_count > 1 AND avg_tenure_months < 18 THEN '🟡 MEDIUM RISK: Serial "Stagnation" Leaver (Boredom Pattern?)'
WHEN short_stints_lt_1yr > 2 THEN '🔴 HIGH RISK: Job Hopper'
WHEN avg_tenure_months >= 36 AND stagnation_count = 1 THEN '🟢 LOW RISK: Long tenure, left for Growth (Healthy)'
ELSE '🟢 LOW RISK: Standard Career Progression'
END AS flight_risk_assessment
FROM patterns;
Резюме (TL;DR для кандидата)
> Ваш ответ «стагнация, хочется роста» — это 3/10. Он пассивен. > > 1. Замените «Хочу» на «Могу / Решал / Строил». Покажите Agency (субъектность). > 2. Объясните, почему не смогли реализовать себя там (бизнес-контекст, блокеры), а не просто «было скучно». > 3. Явно закройте страхи: «Нет PIP, нет конфликтов, отработал notice period, передача подписана». > 4. Свяжите уход с нашей вакансией: «Ушел от Maintenance Mode -> Иду к вашему Highload/Platform/Greenfield». > 5. Подготовьте Impact Map (как в прошлом ответе про зарплату) — это лучшее доказательство, что вы не «убегаете от проблем», а «ищете где быть полезным на 100%».
Вопрос 40. Как идут параллельные собеседования, есть ли другие офферы?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:41:03"/>
Ответ собеседника: правильный. Есть активные процессы: Сбер (прошёл 1-й этап, завтра 2-й алгоритмический), Яндекс (завтра 1-й этап). В Сбере риск, что возьмут внутреннего кандидата, фидбека пока нет.
Правильный ответ:
Ответ кандидата стратегически грамотен (честность + демонстрация спроса на рынке), но тактически незавершен. На уровне Senior/Tech Lead ответ должен не просто констатировать факты, а управлять таймлайнами и формировать FOMO (Fear Of Missing Out) у текущего работодателя, не вызывая отторжения («он уйдет в Яндекс, зачем тратить время»).
Ниже — разбор, как выжать максимум из этой ситуации, как отвечать на уточняющие вопросы и как синхронизировать воронки.
1. Почему текущий ответ — 7/10 (что упущено)
| Аспект | Что сказал кандидат | Что думает основатель (внутренний монолог) | Что нужно добавить |
|---|---|---|---|
| Леверидж (Leverage) | «Есть процессы в Сбере и Яндексе» | «Ок, он востребован. Но это корпы, там долгие офферы (2-4 недели). У нас стартап, мы можем дать оффер быстрее». | Явный таймлайн: «Мой дедлайн по принятию решения — конец следующей недели / конкретная дата. Я хочу закрыть вопрос до [Дата]». |
| Приоритет (Intent) | Не назван явно. | «Он хочет в Яндекс/Сбер за брендом/стабильностью/RSU. Мы ему не конкурент». | Прямое заявление приоритета (если это правда): «Но ваш продукт/стек/скоуп (Highload/ML/Arch Ownership) мне интереснее корп. задач. Если мы сойдемся по терминам — я готов остановить остальные процессы». |
| Статус воронок | «Сбер: 1 этап пройден, завтра алго. Яндекс: завтра 1-й». | «В Сбере после алго еще 2 еще 2-3 этапа (сист. дизайн, хайринг менеджер, HR). В Яндексе еще дольше. Офферы через 3-4 недели минимум». | Реалистичная оценка ETA офферов: «В Сбере до оффера ~2-3 недели при идеальном раскладе. В Яндексе ~3-4 недели. У вас есть окно 1-2 недели, чтобы опередить их». |
| Риск «Внутренний кандидат» (Сбер) | «Риск, что возьмут внутреннего». | «Хорошо, что он реалистичен. Но значит, Сбер — слабый леверидж». | Не оправдываться, а использовать: «Именно поэтому ваш процесс для меня приоритетнее — здесь решение зависит от меня и вас, а не от HR-политики корпорации». |
2. Идеальный сценарий ответа (Senior Negotiation Script)
> Контекст: «Да, я на рынке активно. Сейчас два горячих процесса: Сбер (прошел скрининг, завтра алгоритмический этап) и Яндекс (завтра первый технический экран).» > > Реалистичность (Managing Expectations): «Честно: в Сбере процесс до оффера займет минимум 2.5-3 недели (алго -> системный дизайн -> хайринг менеджер -> оффер-комитет). В Яндексе — 3-4 недели. Плюс в Сбере реальный риск квоты на внутреннего кандидата, так что это не 100% леверидж.» > > Мой Таймлайн (Deadline): «Мой личный дедлайн по принятию финального решения — [Конкретная дата, напр. Пятница следующей недели / 10-е число]. Я не хочу тянуть до конца корп. процессов.» > > Приоритет / Commitment Signal (The Closer): «Но: задача, которую вы решаете [Highload / ML Platform / Greenfield Arch / Team Lead], и стек [Go, K8s, Kafka...] попадают в мои топ-приоритеты точнее, чем типичные корп. роли. Если мы к [Дате дедлайна] сойдемся по компенсации (180к Net + ревью) и скоупу — я готов принять ваш оффер и остановить остальные процессы без отсрочки. Это мой коммитмент.»
3. Гейм-теория: Синхронизация воронок (Timeline Alignment)
Твоя задача — сжать их процесс под твой дедлайн, не дав им почувствовать давление («или я уйду»), а создав дефицит времени («я хочу выбрать вас, но мне нужен ответ до X»).
Твой календарь действий (Action Plan):
| Дата | Событие | Твое действие |
|---|---|---|
| Завтра | Алго Сбер / 1-й Яндекс | Пройти максимально хорошо. После — написать рекрутеру Сбера: «Прошел этап. У меня параллельный процесс у стартапа (финальные стадии) и Яндекс. Мой дедлайн по решению — [Дата]. Можно ли ускорить системный дизайн и хайринг-менеджера на эту неделю?» |
| +1-2 дня | Ожидание фидбека от Сбера / 2-й этап Яндекса | Написать основателю текущей компании: «Прошел алгоритмику в Сбере, жду фидбек. В Яндексе идет процесс. Напоминаю: мой дедлайн [Дата]. Очень хочу к вам, давайте синхронизируем финал на этой неделе». |
| Дедлайн (ДД) | День решения | Идеально: У тебя на руках оффер от текущей компании (подписанный оффер-леттер). Сбер — на стадии «ожидаем комитета» (слабый леверидж). Яндекс — на стадии 2-го этапа. Ты подписываешь оффер текущей компании. |
| План Б (если текущая компания затянула) | Нет оффера к Дедлайну | Пишешь: «К сожалению, у нас не сошлись таймлайны. Я ухожу в процессы Сбера/Яндекса до финала. Если к концу месяца ситуация изменится — пишите, но гарантий нет». Это Hard Walk Away. Работает только если ты реально готов уйти. |
4. Опасные вопросы основателя и как на них отвечать
Вопрос: «А если Яндекс/Сбер дадут больше денег / RSU / бонус за подпись?»
> Неправильно: «Не знаю, посмотрю», «Деньги не главное». > Правильно (Anchoring + Total Comp): > «Смотрите, RSU в корпах — это лотерея с вестингом 4 года и клиффом 1 год. Мой кэш-флоу сейчас — приоритет. 180к Net на руках сегодня > гипотетические RSU через год. > Если Сбер даст 200к Net + Sign-on — я приду к вам с этим числом: "Ребята, Сбер дал 200к, но я хочу к вам. Сможете выбить 190к + ускоренный ревью за 3 мес?" — это честный диалог. Но базовая моя ставка — 180к Net. Я не аукцион, я ищу fair market value для моего скоупа».
Вопрос: «Вы точно к нам придете, если дадим оффер? Не откажете в последний момент?»
> Правильно (Integrity + Condition): > «Мое слово: если оффер соответствует обсужденным терминам (Base 180к Net / Gross 207к, скоуп Tech Lead/Staff, ревью 3 мес, старт [Дата]) — я подписываю в день получения и в тот же час пишу в Сбере/Яндексе отказ. > Если термины будут другими — мы обсуждаем. Но "отказать в последний момент без причины" — не мой стиль, репутация на рынке дороже».
Вопрос: «А зачем вам стартап, если есть Яндекс/Сбер? Стабильность, бенефиты, бренд?»
> Правильно (Motivation Fit): > «В Яндексе/Сбере я буду винтиком в большой машине: узкая специализация, медленные релизы, бюрократия, legacy. Здесь — Ownership end-to-end: архитектура, инфра, найм, продакшн, влияние на продукт. Я хочу строить, а не поддерживать. Риск стартапа понимаю — поэтому прошу 180к (риск-премию) и ревью через 3 месяца (алигнмент).»
5. SQL: Трекер воронок (Candidate Pipeline Tracker)
Веди эту таблицу в Notion/Google Sheets/Excel. Это твой Command Center переговоров.
CREATE TABLE interview_pipeline (
company VARCHAR(50) PRIMARY KEY,
role_scope VARCHAR(50), -- 'Staff Go Eng', 'Tech Lead', 'Backend Senior'
stage_current VARCHAR(50), -- 'Screen Passed', 'Algo Scheduled', 'SysDesign Pending', 'Offer Received'
stage_next VARCHAR(50),
date_next_stage DATE,
eta_offer_weeks INT, -- Реалистичная оценка недель до оффера
probability_offer DECIMAL(3,2), -- 0.0 - 1.0 (твоя субъективная оценка)
expected_tc_net INT, -- Ожидаемый Net (если знаешь вилку)
has_rsu BOOLEAN,
rsu_vesting_years INT,
my_priority INT, -- 1 (Top) -> 5 (Backup)
deadline_decision DATE, -- ТВОЙ ЖЕСТКИЙ ДЕДЛАЙН
commitment_given BOOLEAN DEFAULT FALSE, -- Сказал ли ты "если оффер X -> подписываю"
notes TEXT
);
-- ТЕКУЩЕЕ СОСТОЯНИЕ (Пример заполнения)
INSERT INTO interview_pipeline VALUES
('Current Startup', 'Tech Lead / Staff', 'Final Negotiation', 'Offer Signing', CURRENT_DATE + 2, 0.5, 0.9, 180000, FALSE, 0, 1, CURRENT_DATE + 7, FALSE, 'Дедлайн 10-го числа. Ожидаю оффер до среды.'),
('Sber', 'Senior Go Developer', 'Algo Scheduled', 'System Design', CURRENT_DATE + 1, 3, 0.4, 200000, TRUE, 4, 3, CURRENT_DATE + 21, FALSE, 'Риск внутреннего кандидата. Нужно просить ускорить.'),
('Yandex', 'Backend Developer', 'Screen Scheduled', 'Algo + SysDesign', CURRENT_DATE + 1, 4, 0.3, 220000, TRUE, 4, 4, CURRENT_DATE + 28, FALSE, 'Долгий процесс. Леверидж слабый до оффера.');
-- ЗАПРОС: ЧТО ДЕЛАТЬ СЕЙЧАС (Next Best Action)
SELECT
company,
stage_current,
date_next_stage,
CASE
WHEN company = 'Current Startup' AND deadline_decision <= CURRENT_DATE + 3 THEN '🔴 URGENT: Надавить на оффер / получить вербал'
WHEN stage_next LIKE '%Scheduled%' THEN '🟡 PREP: Подготовиться к ' || stage_next
WHEN probability_offer > 0.5 AND eta_offer_weeks > 2 THEN '🟢 NUDGE: Попросить рекрутера ускорить (параллельный процесс)'
ELSE '🟢 WAIT'
END AS action_required
FROM interview_pipeline
WHERE stage_current NOT IN ('Offer Rejected', 'Offer Accepted')
ORDER BY my_priority, deadline_decision;
6. Психология основателя: Что он слышит в твоем ответе
| Твоя фраза | Что слышит основатель (Decoder) | Как скорректировать сигнал |
|---|---|---|
| «Есть процессы» | «Я горячий товар, плати больше» | Добавь: «Но я не аукцион. У меня есть фаворит — вы, если закроем термины». |
| «Сбер/Яндекс» | «Корпы, долго, стабильно, он уйдет за RSU» | Добавь: «Там 3-4 недели до оффера. Мой дедлайн 1 неделя. Я хочу решить вопрос с вами раньше». |
| «Риск внутреннего кандидата» | «Сбер — блеф, левериджа нет» | Не оправдывайся. Используй: «Именно поэтому ваш процесс чище — решение за нами». |
| «Завтра этапы» | «Он занят, не будет ждать нас долго» | Сигнал срочности: «Мой дедлайн [Дата]. Давайте договоримся о фидбеке от вас до [Дата - 1 день]». |
Резюме (TL;DR)
- Не просто перечисляй компании. Дай контекст: этапы, ETA оффера, вероятность, твой дедлайн.
- Сжимай их таймлайн под свой дедлайн. «Хочу решить до пятницы. Сбер даст оффер через 3 недели. Значит, ваш шанс — только эта неделя».
- Дай Commitment Signal (Сигнал обязательства). «Если оффер = X, я подписываю сразу и отказываю им». Это снимает страх «потратим время, а он уйдет».
- RSU != Cash. Готовь аргументацию, почему 180к Net сейчас > 250к Net + RSU через год (клифф, волатильность, налоги, ликвидность).
- Веди трекер (SQL/Notion). Видишь картину целиком — управляешь процессом, а не реагируешь.
Вопрос 41. В каком направлении хочешь развиваться, какие технологии интересуют?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:42:49"/>
Ответ собеседника: правильный. Интересно поработать с AI на фронте (стриминг ответов), в перспективе — WebSockets, постоянные соединения, прямые трансляции, сложные real-time задачи.
Правильный ответ:
Ответ кандидата показывает понимание современных трендов (LLM Streaming, Real-time), но с позиции Senior/Tech Lead он фрагментарен и слегка «фронтенд-центричен». На собеседовании на Go-разработчика (бэкенд) ожидается, что ты свяжешь эти интересы с серверной архитектурой, инфраструктурой и data-intensive приложениями. «Хочу стримить ответы LLM» — это фича. «Хочу строить низколатентную, отказоустойчивую инфраструктуру для стриминга LLM с управлением бэкпрешшером, контекстом и стоимостью» — это инженерная цель уровня Senior+.
Ниже — как расширить этот ответ до архитектурной программы развития.
1. Карта компетенций: От «Технологий» к «Доменам ответственности» (Senior+ View)
Не просто перечисляй стек. Группируй по проблемным доменам, которые ты хочешь закрывать.
| Твой интерес | Домен развития (Senior/Staff Level) | Ключевые вызовы (Challenges) для изучения | Go-инструментарий / Экосистема |
|---|---|---|---|
| AI Streaming / LLM Inference | ML Platform Engineering / LLM Serving Infra | Token streaming (SSE/WebSocket), Backpressure, KV-cache management, Continuous Batching (vLLM/TGI), Speculative Decoding, Cost optimization (GPU utilization), Observability (TTFT, TPOT). | go-skynet/llama.cpp bindings, tgi client, grpc/connectrpc для model router, prometheus/otel для метрик генерации. |
| WebSockets / Persistent Connections | High-Concurrency Network Programming / Gateways | C10M/C100M problem, Epoll/Kqueue/IO_uring, Connection lifecycle (heartbeats, reconnection, ordering), State synchronization (CRDTs/Operational Transform), Protocol design (WebSocket vs gRPC Stream vs WebTransport). | gobwas/ws (zero-copy), nhooyr/websocket, valyala/gnet / panjf2000/gnet (event-loop), fasthttp WebSocket upgrade. |
| Real-time / Live Streaming | Media Engineering / Distributed Systems | Low latency (WebRTC, SRT, WHIP/WHEP, LL-HLS), Media servers (MediaMTX, LiveKit, Janus), SFU/MCU архитектура, Simulcast/SVC, Bandwidth estimation (BWE), Jitter buffers. | pion/webrtc (Pure Go WebRTC), gortsplib, FFmpeg bindings, gstreamer bindings. |
| Architecture & Scale | Platform Engineering / System Design | Multi-region Active-Active, Service Mesh (Istio/Cilium), GitOps (ArgoCD/Flux), Chaos Engineering, Capacity Planning, Cost Governance (FinOps). | Kubernetes Operators (controller-runtime), Crossplane, Terraform Providers. |
2. Пример «Правильного» ответа (STAR + Technical Depth)
> Контекст: «Мой вектор развития — Data-Intensive & Real-time Backend Systems. Конкретно: построение высоконагруженных шлюзов и платформ для инференса ML и real-time коммуникаций.»
>
> 1. LLM Serving & AI Infra (Hard Skill Focus):
> «Сейчас глубоко копую LLM Serving Stack: vLLM / TGI (Text Generation Inference) архитектуру — Continuous Batching, PagedAttention, Prefix Caching. Интересует задача построения собственного Model Router / Gateway на Go, который решает: умный роутинг (cost/latency), стриминг с backpressure (SSE/WebSocket), управление контекстом (sliding window, RAG injection), observability (TTFT, TPOT, queue time).
> Пример задачи: Реализовать прокси на Go, который агрегирует стримы от нескольких моделей, делает fallback на маленькую модель при таймауте большой, и корректно прокидывает Cancel клиента в context.Context генерации для освобождения GPU слотов.»
>
> 2. Real-time Transport & Concurrency (Core Go Expertise):
> «Параллельно углубляюсь в Network Programming на Go: сравнение netpoll (epoll/kqueue) vs io_uring (через github.com/iceber/iouring-go или gnet) для удержания 100k+ WebSocket соединений на инстансе. Интересна проблема State Sync в распределенном чате/стриминге: CRDTs (Yjs/Automerge port на Go) vs Operational Transform vs Server-Authoritative State с Event Sourcing.»
>
> 3. Media & WebRTC (Domain Expansion):
> «В перспективе — Media Server Internals (SFU на pion/webrtc): Simulcast/SVC routing, NACK/PLI handling, Bandwidth Estimation (GCC/WEBRTC BWE), интеграция с HW-кодеками (VA-API/NVENC via FFmpeg). Понимаю, что чистый Go для медиа-плана не всегда лучший выбор (CGOs overhead), поэтому смотрю в сторону архитектуры: Go Control Plane + Rust/C++ Media Plane (MediaMTX / LiveKit подход).»
>
> 4. Platform & Reliability (Staff/Lead Scope):
> «Но технологии бесполезны без надежности. Хочу прокачать Platform Engineering: написание Kubernetes Operators (controller-runtime) для управления стейтфул-сервисами (Redis, ClickHouse, Media Nodes), внедрение GitOps (ArgoCD), Chaos Engineering (Litmus/Chaos Mesh) для проверки отказоустойчивости real-time пайплайнов.»
3. Go Code Snippets: Демонстрация глубины по темам ответа
А. Правильный стриминг LLM с Backpressure и Cancellation (Senior Pattern)
package llmgateway
import (
"context"
"io"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/valyala/bytebufferpool"
)
// StreamHandler обрабатывает запрос на генерацию с SSE стримингом.
// Ключевые моменты Senior:
// 1. context.Context прокидывается ВНИЗ в модель (отмена генерации при отвале клиента).
// 2. Flusher используется явно для контроля буферизации (low latency).
// 3. Backpressure: читаем от модели -> пишем в сокет. Если сокет забит (клиент медленный) -> контекст отменяется -> GPU освобождается.
// 4. Метрики: TTFT (Time To First Token), TPOT (Time Per Output Token).
func (h *Handler) StreamHandler(c *gin.Context) {
var req GenerateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 1. Создаем контекст с таймаутом всего запроса (например, 5 минут)
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Minute)
defer cancel()
// 2. Вызываем Model Router / Inference Engine
// Предполагаем, что ModelClient возвращает канал токенов или error
tokenCh, errCh := h.modelClient.GenerateStream(ctx, req)
// 3. SSE Headers (Critical for streaming)
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("X-Accel-Buffering", "no") // Disable Nginx buffering
flusher, ok := c.Writer.(http.Flusher)
if !ok {
h.log.Error("streaming unsupported")
return
}
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
firstToken := true
ttftStart := time.Now()
for {
select {
case <-ctx.Done():
// Клиент отключился (c.Request.Context().Done()) ИЛИ общий таймаут
h.metrics.ObserveClientDisconnect(req.Model)
return
case err := <-errCh:
if err != nil && err != context.Canceled {
h.log.Error("generation error", "err", err)
// Отправляем error event в SSE
fmt.Fprintf(c.Writer, "event: error\ndata: %s\n\n", err.Error())
flusher.Flush()
}
return
case token, ok := <-tokenCh:
if !ok {
// Поток закрыт моделью (EOS)
fmt.Fprint(c.Writer, "event: done\ndata: [DONE]\n\n")
flusher.Flush()
return
}
// Метрика TTFT
if firstToken {
h.metrics.ObserveTTFT(req.Model, time.Since(ttftStart))
firstToken = false
}
// SSE Format: data: <json>\n\n
// Используем буфер для избежания аллокаций
buf.Reset()
buf.WriteString("data: ")
// Быстрая сериализация (например, sonic/go-json)
_ = json.NewEncoder(buf).Encode(map[string]string{"token": token})
buf.WriteString("\n\n")
// Пишем в ResponseWriter
_, err := c.Writer.Write(buf.Bytes())
if err != nil {
// Клиент ушел (Broken pipe) -> ctx отменится на следующей итерации select
return
}
// Явный Flush = Low Latency (Critical for UX)
flusher.Flush()
}
}
}
Б. WebSocket Hub с шардированием и Backpressure (C100k Pattern)
package realtime
import (
"context"
"sync"
"time"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
// Hub управляет пулом соединений.
// Паттерн: Шардирование по UserID/RoomID для локальности блокировок.
type Hub struct {
shards []*Shard
shardCount int
register chan *Client
unregister chan *Client
broadcast chan *Message
}
type Shard struct {
mu sync.RWMutex
clients map[string]*Client // key: userID
}
func NewHub(shardCount int) *Hub {
h := &Hub{
shardCount: shardCount,
shards: make([]*Shard, shardCount),
register: make(chan *Client, 1024),
unregister: make(chan *Client, 1024),
broadcast: make(chan *Message, 4096),
}
for i := 0; i < shardCount; i++ {
h.shards[i] = &Shard{clients: make(map[string]*Client)}
}
return h
}
func (h *Hub) getShard(key string) *Shard {
// FNV Hash для равномерного распределения
hsh := fnv32(key)
return h.shards[hsh%uint32(h.shardCount)]
}
// Run запускает event-loop хаба (один на процесс, или на шард для масштаба)
func (h *Hub) Run(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case client := <-h.register:
shard := h.getShard(client.UserID)
shard.mu.Lock()
shard.clients[client.UserID] = client
shard.mu.Unlock()
case client := <-h.unregister:
shard := h.getShard(client.UserID)
shard.mu.Lock()
if c, ok := shard.clients[client.UserID]; ok && c == client {
delete(shard.clients, client.UserID)
close(client.Send) // Сигнал writer-рутине клиента
}
shard.mu.Unlock()
case msg := <-h.broadcast:
// Fan-out: отправляем только в нужный шард/комнату
// В реальности нужен Room/Topic index
targetShard := h.getShard(msg.RoomID)
targetShard.mu.RLock()
for _, client := range targetShard.clients {
select {
case client.Send <- msg.Payload:
default:
// Backpressure: буфер клиента полон (Slow Consumer)
// Закрываем соединение, чтобы не забить память сервера
go client.Close() // Асинхронно, чтобы не блокировать broadcast loop
}
}
targetShard.mu.RUnlock()
}
}
}
// Client представляет одно WS соединение.
// Используем gobwas/ws для zero-copy и контроля фреймов.
type Client struct {
Conn net.Conn
Send chan []byte // Buffered channel = Backpressure buffer
UserID string
Hub *Hub
}
func (c *Client) WritePump(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second) // Ping interval
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case data, ok := <-c.Send:
if !ok {
wsutil.WriteServerMessage(c.Conn, ws.OpClose, nil)
return
}
// Deadline для записи (защита от зависания)
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := wsutil.WriteServerMessage(c.Conn, ws.OpText, data); err != nil {
return
}
case <-ticker.C:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := wsutil.WriteServerMessage(c.Conn, ws.OpPing, nil); err != nil {
return
}
}
}
}
4. SQL: Схема для хранения контекста LLM и Real-time сессий
-- 1. LLM Conversation Context Management (RAG / History)
CREATE TABLE llm_conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
model_name VARCHAR(100) NOT NULL, -- 'gpt-4o', 'llama-3-70b-instruct'
system_prompt TEXT,
-- Храним сообщения в JSONB для гибкости (tool_calls, tool_results, images)
messages JSONB NOT NULL DEFAULT '[]'::jsonb,
-- Метаданные для роутинга и стоимости
total_tokens_in INT DEFAULT 0,
total_tokens_out INT DEFAULT 0,
status VARCHAR(20) DEFAULT 'active', -- 'active', 'summarized', 'archived'
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Индекс для поиска активных контекстов пользователя
CREATE INDEX idx_llm_conv_user_status ON llm_conversations(user_id, status) WHERE status = 'active';
-- 2. Real-time Sessions (WebSocket / WebRTC Signaling)
CREATE TABLE rt_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
session_type VARCHAR(20) NOT NULL, -- 'websocket', 'webrtc_sfu', 'sip'
gateway_node VARCHAR(100), -- ID ноды балансировщика (для sticky sessions)
-- Состояние подключения
state VARCHAR(20) DEFAULT 'connecting', -- 'connected', 'disconnected', 'reconnecting'
-- WebRTC Specific
sdp_offer TEXT,
sdp_answer TEXT,
ice_candidates JSONB,
-- Медиа-параметры
codec_prefs VARCHAR(50)[], -- ['VP8', 'H264', 'OPUS']
simulcast_layers INT,
-- Статистика (агрегируется фоновым воркером)
bytes_sent BIGINT DEFAULT 0,
bytes_recv BIGINT DEFAULT 0,
packets_lost INT DEFAULT 0,
rtt_ms INT,
jitter_ms INT,
started_at TIMESTAMPTZ DEFAULT now(),
ended_at TIMESTAMPTZ
);
-- Партиционирование по времени для горячих данных (TimescaleDB или нативное PG partitioning)
-- CREATE TABLE rt_sessions PARTITION BY RANGE (started_at);
-- 3. Metrics для Observability (TTFT, TPOT, Concurrency)
CREATE TABLE llm_inference_metrics (
id BIGSERIAL PRIMARY KEY,
conversation_id UUID REFERENCES llm_conversations(id),
request_id UUID NOT NULL, -- Trace ID
model_name VARCHAR(100) NOT NULL,
-- Latency breakdown
ttft_ms INT, -- Time To First Token
tpot_ms NUMERIC(10,2), -- Time Per Output Token (avg)
e2e_latency_ms INT, -- End-to-end
-- Throughput
prompt_tokens INT,
completion_tokens INT,
-- System
queue_time_ms INT, -- Время в очереди к GPU
gpu_id VARCHAR(50),
status VARCHAR(20), -- 'success', 'timeout', 'cancelled', 'oom'
created_at TIMESTAMPTZ DEFAULT now()
) PARTITION BY RANGE (created_at);
-- Гипертаблица TimescaleDB для автоматического партиционирования
SELECT create_hypertable('llm_inference_metrics', 'created_at', chunk_time_interval => INTERVAL '1 day');
5. План развития (Roadmap) — что сказать в конце ответа
> «Мой план на ближайшие 6-12 месяцев:»
> 1. Глубина (Go Internals): Профилирование (pprof, go tool trace) real-time сервисов под нагрузкой: оптимизация GC (GOGC, runtime/debug.SetMemoryLimit), escape analysis, lock-free структуры (sync/atomic, atomic.Pointer).
> 2. Инфраструктура (K8s Operators): Написание Operator'а для управления пулом GPU-инстансов с LLM (автоскейлинг по длине очереди, scale-to-zero для редких моделей).
> 3. Домен (WebRTC/SFU): Контрибьют в pion/webrtc или livekit — реализация Simulcast/SVC routing logic на Go Control Plane.
> 4. Архитектура (System Design): Проектирование Multi-Region Active-Active для Real-time: конфликты состояния (CRDTs), гео-роутинг (GeoDNS/Anycast), репликация WebSocket сессий.
Резюме (TL;DR)
> Не говори просто «WebSockets и AI». > Говори: «Строю высоконагруженные Gateways для LLM Streaming (Backpressure, Cancellation, Observability) и Real-time Communication Platforms (WebSocket/WebRTC SFU, State Sync, C100k на Go)». > > * AI -> ML Platform Eng (vLLM/TGI, Router, Cost/Latency Tradeoffs). > * WebSockets -> Network Eng (io_uring/gnet, Backpressure, Sharding, Protocol Design). > * Real-time -> Distributed Systems (WebRTC/SFU, CRDTs, Media Pipeline, Multi-region). > * Stack -> Go (Control Plane) + Rust/C++ (Data Plane) + K8s (Orchestration) + Observability (OTel/Prom/Grafana).
Вопрос 42. Готов ли подождать до субботы, пока мы обдумаем возможность повышения оффера до 180к?
Таймкод: <YouTubeSeekTo id="UXZq4Nb_yaY" time="00:45:22"/>
Ответ собеседника: правильный. Согласен, остаёмся на связи до субботы. Основатель предупредил, что вряд ли перебьёт офферы Сбера/Яндекса, но если там не сложится — вернётся к обсуждению.
Правильный ответ:
Ответ кандидата операционно верный (не сжёг мосты, выиграл время), но стратегически слабый. Ты принял пассивную роль «ожидающего», позволил основателю установить низкий якорь («вряд ли перебьём») и не зафиксировал коммитмент со стороны компании. На уровне Senior/Tech Lead переговоры — это про управление неопределенностью и рисками, а не про пассивное согласие.
Ниже — разбор ошибок, правильный скрипт ответа и тактика на субботу.
1. Разбор ошибок текущего ответа (Post-Mortem)
| Ошибка | Последствие | Как исправить |
|---|---|---|
| Принял фрейм «вряд ли перебьём» | Основатель снизил твою ценность до принятия решения. Ты согласился с тем, что их оффер — «план Б». | Никогда не соглашайся с негативной оценкой своей стоимости. Ответ: «Понимаю риски. Моя задача — принять лучшее решение для себя к [Дата]. Ваша задача — дать лучший оффер, который вы можете承担ть». |
| Нет дедлайна принятия решения (Decision Deadline) | «До субботы» — это их дедлайн на обдумывание. Твой дедлайн на подписание отсутствует. Они могут затянуть до понедельника/вторника. | Установи Hard Deadline: «Я готов ждать до субботы 14:00. В 14:00 я принимаю решение. Если оффера нет — я ухожу в процессы Сбера/Яндекса до финала». |
| Нет вербального коммитмента (Verbal Commitment) | Ты не получил обещания: «Если дадим 180к — ты подписываешь в субботу?». Они могут дать 180к в субботу, а ты скажешь «нужно подумать/Сбер дал больше». | Забери коммитмент: «Если в субботу на столе 180к Net + ревью 3 мес + скоуп Tech Lead — я готов подписать оффер-леттер в тот же день и писать отказы Сберу/Яндексу. Готовы дать такое обещание?» |
| Игнор параллельных процессов (BATNA) | Ты не напомнил, что в Сбере завтра алгоритмика, в Яндексе скрининг. Твоя альтернатива (BATNA) растет каждый день. | Напомни про таймлайн корп: «В Сбере завтра этап, в Яндексе — тоже. Их процессы идут быстрее, чем вы думаете. Мой окно решения — эти выходные». |
2. Идеальный скрипт ответа (Senior Negotiation Script)
> Тон: Спокойный, деловой, твердый, но партнерский. > > Скрипт: > «Хорошо, давайте зафиксируем правила игры на эти выходные, чтобы не тратить время впустую у обеих сторон. > > 1. Дедлайн (Hard Stop): > Я готов ждать ваш финальный оффер до субботы 14:00 (МСК). В 14:00 я принимаю решение: либо подписываю у вас, либо коммитчусь в корп. процессы (Сбер/Яндекс) и иду до оффера там. Продление до понедельника невозможно — мне нужно закрыть вопрос до новой недели. > > 2. Коммитмент (If-Then): > Если в субботу 14:00 вы присылаете оффер-леттер на 180к Net (на руки) + ревью зарплаты через 3 месяца + закрепление скоупа Tech Lead / Staff — я подписываю его в субботу же и в течение часа отправляю официальные отказы в Сбер и Яндекс. Мое слово — это действие. > > 3. Реализм по корп. офферам: > По поводу «вряд ли перебьем»: в Сбере/Яндексе базовые оклады Senior/Staff сейчас 180-220к Net + RSU + Sign-on бонусы 100-300к. Я не прошу у вас RSU, я прошу кэш-равенство. 180к Net — это fair market value для моего скоупа на рынке сейчас, а не «перебивание» корп. офферов. Если вы не можете выйти на 180к Net — скажите честно в пятницу вечером, я не буду заставлять вас ждать до субботы. > > 4. Следующий шаг: > Давайте договоримся: в пятницу 18:00 вы даете мне короткий статус («согласовали / не согласовали / есть вопросы»), чтобы я знал, стоит ли ждать субботу. Если в пятницу молча — я считаю, что 180к не проходит, и активизирую Сбер/Яндекс с утра в субботу».
3. Гейм-теория: Что происходит в голове основателя (Founder's Mindset)
| Сигнал от тебя | Как читает основатель | Твоя цель |
|---|---|---|
| «Ок, жду до субботы» | «У него нет других вариантов / он блефует / можно затянуть до понедельника / можно дать 170к». | Сигнал дефицита времени: «У меня есть альтернатива, которая требует решения сейчас. Ваш шанс — только эти 48 часов». |
| «Если там не сложится — вернусь» | «Он в "пайплайне" на резерве. План Б». | Сигнал приоритета: «Вы — мой План А при условии терминов. Без терминов — вы План Б». |
| Молчание про Sign-on / RSU | «Он не знает рынок / стесняется просить». | Якорь Total Comp: «Рынок платит Sign-on 100-300к. Если нет RSU — база должна быть выше или нужен Sign-on». |
4. Чек-лист на пятницу/субботу (Action Plan)
Пятница (день до дедлайна)
- 12:00 Написать основателю: «Коллеги, напоминаю про дедлайн суббота 14:00. Есть ли движение по апруву 180к? Нужен статус к 18:00 сегодня, чтобы я мог планировать выходные (готовиться к алго в Сбере / отдыхать / подписывать у вас)».
- 18:00 Если ответа нет / «еще не решили» -> «Ок, значит 180к не проходит или затягивается. Считаю переговоры закрыты. Удачи». Не жди субботу. Твоя репутация — это твои границы.
- Параллельно: Пройти алго в Сбере максимально круто. Написать рекрутеру Сбера: «Ускорьте процесс, у меня дедлайн по другому офферу выходные».
Суббота (Decision Day)
| Сценарий | Твое действие |
|---|---|
| Прислали оффер-леттер 180к Net + Review 3m + Scope | Подписываешь мгновенно (DocuSign/PDF). Пишешь в Сбер/Яндекс: «Принял другое предложение, отказываюсь от процесса. Спасибо за время». |
| Прислали 170-175к / «попробуем через полугод» / «нет ревью» | «Не подходит». Пишешь: «Уважаю время, но термины не соответствуют рынку и моим запросам. Принимаю оффер Сбера/продолжаю процессы. Если ситуация изменится — пишите, но гарантий нет». |
| Молчат до 14:00 | В 14:01 пишешь: «Дедлайн прошел. Оффера нет. Переговоры закрыты. Удачи команде». Блокируешь контакты (опционально, но психологически важно). |
5. SQL: Состояние переговоров (Negotiation State Machine)
Веди этот трекер. Он заставляет тебя оперировать фактами, а не эмоциями.
CREATE TABLE negotiation_log (
id BIGSERIAL PRIMARY KEY,
company VARCHAR(50) NOT NULL, -- 'Current Startup', 'Sber', 'Yandex'
stage VARCHAR(50) NOT NULL, -- 'Verbal Offer', 'Waiting Counteroffer', 'Hard Deadline Set', 'Signed', 'Rejected'
base_net INT, -- Net salary offered
review_months INT, -- Guaranteed review timeline
sign_on_bonus INT DEFAULT 0,
rsu_value INT DEFAULT 0, -- Estimated annual RSU value
deadline_ts TIMESTAMPTZ, -- YOUR hard deadline
commitment_given BOOLEAN DEFAULT FALSE, -- Did you say "If X -> Sign"?
commitment_received BOOLEAN DEFAULT FALSE, -- Did they say "If X -> We Give"?
notes TEXT,
updated_at TIMESTAMPTZ DEFAULT now()
);
-- ТЕКУЩЕЕ СОСТОЯНИЕ (после разговора в 00:45:22)
INSERT INTO negotiation_log (company, stage, base_net, review_months, deadline_ts, commitment_given, commitment_received, notes) VALUES
('Current Startup', 'Waiting Counteroffer', 180000, 3, '2023-10-28 14:00:00+03', FALSE, FALSE, 'Founder said "unlikely to beat corp". Need to lock commitment by Fri 18:00. Hard stop Sat 14:00.'),
('Sber', 'Algo Stage Tomorrow', 200000, 12, NULL, FALSE, FALSE, 'Algo interview tomorrow. Need to ask for accelerated System Design.'),
('Yandex', 'Screen Tomorrow', 220000, 12, NULL, FALSE, FALSE, 'First screen tomorrow. Long process.');
-- ЗАПРОС: ЧТО ДЕЛАТЬ СЕЙЧАС (Next Best Action)
SELECT
company,
stage,
base_net,
deadline_ts,
CASE
WHEN company = 'Current Startup' AND deadline_ts <= NOW() + INTERVAL '24 hours'
AND commitment_received = FALSE
THEN '🔴 URGENT: Send "Friday 18:00 Status" ultimatum. Prepare walk-away email.'
WHEN company = 'Current Startup' AND deadline_ts <= NOW()
THEN '🟢 WALK AWAY: Deadline passed. Send rejection. Focus on Sber/Yandex.'
WHEN stage LIKE '%Tomorrow' THEN '🟡 PREP: Interview prep tonight. Signal urgency to recruiters.'
ELSE '🟢 MONITOR'
END AS action_required
FROM negotiation_log
WHERE stage NOT IN ('Signed', 'Rejected')
ORDER BY
CASE WHEN company = 'Current Startup' THEN 1 ELSE 2 END,
deadline_ts NULLS LAST;
6. Психологические ловушки, в которые НЕ попадаем
| Ловушка | Мыслительный вирус | Антидот (Твоя мысль) |
|---|---|---|
| Sunk Cost (Потраченное время) | «Я уже столько времени потратил на собеседования у них, не могу просто уйти». | «Время уже потрачено (sunk cost). Решаю только по будущей ценности: дадут 180к+коммитмент — остаюсь. Нет — ухожу без сожаления». |
| Loss Aversion (Страх потери) | «А вдруг Сбер/Яндекс не дадут оффер? Лучше синица в руках». | «Мой BATNA (Best Alternative) — это процесс в Сбере/Яндексе, а не оффер. Процесс у меня запущен. Я управляю риском дедлайном». |
| Good Guy Syndrome (Хороший парень) | «Они хорошие ребята, неудобно их подставлять в последний момент». | «Это бизнес. Они бы уволили за 2 недели по ТК РФ. Честное предупреждение за 48 часов — это профстандарт». |
| Anchoring (Якорение на 180к) | «Они предложили 180к, это максимум, больше не попрошу». | «180к — это минимум для входа в игру. Если дадут — ок. Если нет — ухожу. Не торгуюсь вниз от 180к». |
Резюме (TL;DR)
- Ты согласился ждать — ок. Но в пятницу 18:00 требуешь статус. Без статуса — уходи в субботу утром.
- Забери вербальный коммитмент: «Если 180к + ревью 3м + скоуп — подписываю в субботу, отказы в корпы улетают в тот же час».
- Не верь «вряд ли перебьем». 180к Net — это рынок, не лакшери. Если не могут — говорят «нет» в пятницу.
- Hard Deadline: Суббота 14:00. Не 15:00, не понедельник. В 14:01 — ты в другом процессе.
- Параллельно качай Сбер/Яндекс. Твоя сила — в реальной альтернативе, а не в блефе.
Помни: Переговоры заканчиваются не когда ты согласился ждать, а когда ты получил подписанный оффер-леттер или ушел. До субботы 14:00 ты в состоянии «Schrödinger's Offer» — и оффера нет. Действуй исходя из этого.
