Банк питань порталу влаштований так: одне питання — одна картка з рівнем (junior, middle, senior), реальними формулюваннями «як питають», критеріями «що хоче почути інтервʼюер» і списком типових помилок. Зараз карток 52: 13 junior, 25 middle, 14 senior. Це достатній обсяг, щоб покласти senior-питання поруч із middle і подивитися, чим вони відрізняються по суті. Нижче — те, що з цього видно.
Senior-питань найменше, і жодне з них не «часто питають»
Позначку «часто питають» мають 23 картки з 52. Серед них немає жодної senior. Усі часті питання — junior і middle: життєвий цикл запиту в Laravel, service container, N+1, middleware, Form Request, == проти ===, declare(strict_types=1), читання EXPLAIN, INNER проти LEFT JOIN, actions і filters у WordPress, DI-контейнер Symfony.
Висновок неприємний для тих, хто готується до senior-співбесіди «по-senior»: базові питання нікуди не зникають. Розмова на senior починається з тих самих частих карток, просто на них дають менше часу й не пробачають плавання. Різниця не в тому, що вас перестають питати про eager loading. Різниця в тому, що після eager loading іде ще пʼять питань, яких middle не отримує взагалі.
Middle пояснює механіку, senior пояснює збій
Найпомітніша відмінність — навіть не тема, а формулювання. Ось як звучать питання в middle-картках:
- «Чому сторінка зі списком робить 200 запитів до бази?»
- «Що таке eager loading і коли він шкодить?»
А ось senior-картки:
- «Чому під Octane один користувач бачить дані іншого?»
- «Чому воркер черги росте в памʼяті, хоча кожен job невеликий?»
- «Чому два паралельні запити списали з балансу більше, ніж там було?»
- «Клієнт натиснув „Оплатити“ двічі: як не списати гроші двічі?»
- «Debugbar каже 1.8 с, а користувач скаржиться на 8 с. Хто з них бреше?»
Middle-питання закривається описом механізму: розказав, як працює lazy loading, назвав with() — тема пройдена. Senior-питання описує симптом системи, яка вже в продакшені, і очікує, що ви розкладете його на складові й перевірите гіпотези по черзі. У картці про профілювання це прямо сформульовано як процедура: розкласти час (браузер → TTFB → PHP → база → зовнішні виклики), знайти найбільший доданок, змінити одну річ, виміряти ще раз. Оцінюють не назву профайлера, а наявність послідовності — і те, що ви не починаєте з гіпотези «мабуть, треба кеш».
Три теми, яких у middle майже немає
Senior-картки збираються в три кластери, які на middle-співбесідах не звучать зовсім.
Одночасність. Рівні ізоляції, блокування, ідемпотентність. Ключова теза, за яку ставлять плюс: рівень ізоляції сам по собі не закриває втрачене оновлення. Патерн «прочитати в PHP, порахувати, записати» ламається на всьому, що нижче за SERIALIZABLE, а лікується він атомарним UPDATE з умовою або SELECT ... FOR UPDATE.
// Класична відповідь middle: транзакція. Вона тут не рятує —
// обидва процеси прочитали однаковий баланс і однаково його перезаписали.
$account = Account::find($id);
$account->balance -= $amount;
$account->save();
// Рішення приймає база, а не PHP: умова і віднімання в одному операторі
$updated = Account::whereKey($id)
->where('balance', '>=', $amount)
->update(['balance' => DB::raw('balance - '.(int) $amount)]);
if ($updated === 0) {
// нуль оновлених рядків — це бізнес-відповідь «не вистачило коштів», а не збій
throw new InsufficientFunds();
}
Продовження цього кластера — ідемпотентність: клієнтський ключ на одну бізнес-операцію, збережений разом із результатом під унікальним індексом, і розуміння, що перевірка if (! exists) у коді гонку не закриває, а доставка вебхуків і повідомлень черги гарантована щонайменше один раз, тобто повтори неминучі за визначенням.
Стан, який живе довше за один запит. Під php-fpm процес помирає після відповіді, і це прощає багато чого. Під Octane (Swoole, RoadRunner, FrankenPHP) чи в довгому воркері черги — ні.
// Під php-fpm обидва рядки поводяться однаково.
// Під Octane перший — це витік даних між користувачами:
// інстанс переживає запит разом із чужим Request усередині.
$this->app->singleton(CurrentTenant::class, fn ($app) => new CurrentTenant($app['request']));
// scoped — той самий біндінг, але контейнер скидає його на кожному запиті
$this->app->scoped(CurrentTenant::class, fn ($app) => new CurrentTenant($app['request']));
Сюди ж — три джерела витоку стану, які має назвати senior: синглтони в контейнері, статичні властивості класів і memoization у хелперах або глобальних змінних. І механіка памʼяті: PHP звільняє обʼєкт миттєво, щойно refcount падає до нуля, а окремий збирач циклів запускається не за таймером, а за заповненням root buffer (10 000 можливих коренів, поріг адаптивний із PHP 7.3). Тому «воркер росте в памʼяті» — це майже завжди накопичений статичний масив або нескинутий реєстр слухачів, а не проблема GC.
Зміна схеми під живим трафіком. Expand and contract, і розуміння, що під час деплою стара й нова версія коду одночасно працюють з однією базою. Перейменування колонки без простою — це чотири релізи: нова nullable-колонка, подвійний запис плюс backfill батчами, перемикання читання, видалення старої. Плюс знання, який DDL блокує таблицю: NOT NULL без default на великій таблиці, зміна типу, індекс без CONCURRENTLY у PostgreSQL. І окреме питання, на якому валяться навіть сильні кандидати: де запускаються міграції — окремим кроком до перемикання трафіку, а не в entrypoint кожної репліки.
Ціна рішення — обовʼязкова частина senior-відповіді
Це найчіткіший маркер грейду в усьому банку. У senior-картках критерії «що хоче почути інтервʼюер» майже завжди містять межу застосовності:
- Octane нічого не компілює і не робить ваш код швидшим — він прибирає бутстрап фреймворку з кожного запиту, і за це ви платите станом, який більше не скидається сам.
- Черга нічого не пришвидшує — вона переносить час на воркер і додає вам стан «в обробці» в UI, ідемпотентність,
tries,backoff,failed_jobsі моніторинг довжини черги. - Кеш навколо повільного місця — не діагностика: після інвалідації, деплою і кожного нового фільтра ті самі чотири секунди повертаються.
- Картка «Коли DDD виправданий» прямо вимагає прикладу, де ви свідомо DDD не застосували, і чому анемічна модель із транзакційним скриптом там була правильним вибором.
Middle-картки такого не просять — там достатньо знати, що робить with() і чим від нього відрізняється load(). Senior-відповідь без ціни звучить як реклама технології, і саме на цьому валяться кандидати, які вивчили нову річ, але ще не платили за неї в продакшені.
Питання, у яких немає правильної відповіді
Окремий клас senior-питань — архітектурний вибір: Livewire чи API з SPA, дії й модулі чи стандартна структура, репозиторій над Eloquent чи ні. Тут інтервʼюер не тримає в голові еталонну відповідь; він перевіряє, чи є у вас критерії.
Найгірша відповідь — «залежить» без продовження. Робоча — назвати осі рішення вголос і пройтися по них. Для фронтенду: чи існує другий клієнт (якщо так, API вже в кошторисі й SPA дешевшає); чи є взаємодії частіші за клік (вони не переживуть round-trip); хто чергує вночі (команда без фронтенд-компетенції платитиме за SPA щодня). Для структури проєкту критерій ще коротший: у кожного бізнес-сценарію має бути рівно одне місце, куди по нього приходять — а далі сходинки від «контролер плюс модель» до модуля з власним провайдером і арх-тестом.
Показова пара карток — Form Request і DTO. Junior-рівень: як винести валідацію з контролера. Senior-рівень: чому App\Http\Requests\... не можна тягнути в джобу чи консольну команду.
// Контракт сценарію без Illuminate: його однаково будує контролер,
// artisan-команда і споживач черги — сценарій лишається викликуваним поза HTTP.
final readonly class NewSubscription
{
public function __construct(
public int $customerId,
public string $planCode,
public ?string $promoCode = null,
) {}
}
Як цим користуватись у підготовці
- Пройдіть middle-підбірку на швидкість. Якщо на частому питанні ви думаєте більше пів хвилини, це діра, яку помітять першою.
- Візьміть senior-підбірку і для кожної картки сформулюйте не лише відповідь, а й ціну рішення та випадок, коли ви його свідомо не застосували.
- Для трьох senior-кластерів — одночасність, стан у довгих процесах, міграції під трафіком — підготуйте по одному реальному випадку з власної практики. Саме їх просять картки про ізоляцію транзакцій, GC і Octane.
- Не міняйте базу на «senior-теми»: 23 з 52 карток позначені як часті, і senior серед них немає жодної.