Разберем вопросы и мои ответы на них с недавнего интервью в одну Финтех компанию ….
В анализе мне поможет бесплатная , но мощная нейронка «DeepSeek«.
Поехали!
Отчет по собеседованию на должность «Разработчик PHP»
Вопрос 1
«Как правильно работать с деньгами в финтех системах? И как правильно хранить баланс пользователя и проводить финансовые операции над этим балансом.»
Ответ кандидата (суть):
Нужно использовать транзакции для атомарности (всё или ничего), журналировать все операции для восстановления цепочки событий.
Деньги нельзя хранить во float, так как возникают проблемы с округлением – лучше хранить в integer (копейки).
Также нужно проверять лимиты до списания, обеспечить идемпотентность (повторный запрос не должен списывать дважды), можно использовать очереди для асинхронного выполнения.
Оценка:
Ответ верен, но неполный и местами неаккуратен. Кандидат упомянул ключевые практики, но не структурировал их и упустил важные детали (например, использование `DECIMAL` в БД, работу с денежными единицами через Value Object, проблему конкурентного обновления баланса). Ответ «творческий» и разрозненный.
Мой вариант ответа:
В финтехе критичны:
1. Атомарность и транзакционность – операции должны выполняться в БД-транзакциях с уровнем изоляции не ниже `REPEATABLE READ` или `SERIALIZABLE`.
2. Идемпотентность – каждый запрос должен иметь уникальный ключ, чтобы повторные вызовы не приводили к двойному списанию.
3. Хранение – использовать целые числа (копейки/центы) или тип `DECIMAL(N,2)` в БД, никогда не использовать `float/double`.
4. Журналирование – каждое изменение записывается в отдельную таблицу операций (event sourcing), чтобы можно было восстановить баланс и аудировать.
5. Пессимистическая блокировка строки баланса (`SELECT … FOR UPDATE`) при списании, чтобы избежать race condition.
6. Проверка достаточности средств — до начала списания в той же транзакции.
7. Для высоких нагрузок – шардирование балансов или асинхронные очереди с компенсирующими транзакциями.
—
Вопрос 2
«Если 2 параллельных запроса пытаются изменить баланс 1 и того же пользователя. Какие проблемы могут возникнуть? И какое значение в этом сценарии играют транзакции, и как повлияют разные уровни изоляции.»
Ответ кандидата (суть):
Может возникнуть race condition, ведущий к неверному балансу или двойному списанию.
Транзакции и уровни изоляции это контролируют.
Упомянул грязное чтение, фантомное чтение, неповторяемое чтение.
Serializable – самый надежный, но медленный; read uncommitted – ненадежный; repeatable read – допускает фантомы.
На практике выбирают компромисс.
Оценка:
Ответ в целом верен, но кандидат не дал конкретики по механизмам блокировок, не сказал, что именно нужно делать на уровне кода (например, `FOR UPDATE`), и не объяснил, как разные уровни изоляции влияют именно на параллельное обновление одной строки. Упомянул фантомы, но они менее релевантны для обновления одной записи.
Мой вариант ответа:
При параллельных запросах на обновление одной строки баланса возникает потеря обновления (lost update) – оба читают старый баланс, изменяют его и записывают, перетирая изменения друг друга.
Транзакции решают это:
— READ UNCOMMITTED – допускает все аномалии, не подходит.
— READ COMMITTED – предотвращает грязное чтение, но не защищает от потерянных обновлений (без `FOR UPDATE`).
— REPEATABLE READ (стандарт MySQL) – предотвращает неповторяемое чтение, но потеря обновлений возможна, если нет блокировки.
— SERIALIZABLE – последовательное выполнение, исключает все аномалии, но сильно снижает производительность.
На практике используют `SELECT … FOR UPDATE` (пессимистичная блокировка) в транзакции с уровнем `READ COMMITTED` или `REPEATABLE READ`, либо оптимистичную блокировку с версионированием (проверка `version` при обновлении).
—
Вопрос 3
«Есть 2 сервиса, UserService и OrderService. При создании заказа OrderService должен получить какую-то информацию о пользователе. Как построить такое взаимодействие и какие компромиссы можно встретить при реализации.»
Ответ кандидата (суть):
Два подхода:
— Синхронный – OrderService делает запрос к UserService.
Плюс: простота, актуальность данных.
Минусы: задержки, зависимость от доступности UserService, снижение отказоустойчивости.
— Асинхронный – через очереди/события, данные кэшируются.
Плюс: меньше задержек, минус: возможна неактуальность данных, нужно обновлять кэш по событиям.
Оценка:
Ответ верен, но поверхностен. Кандидат не упомянул паттерны API Gateway, Circuit Breaker, Retry, Fallback, не обсудил согласованность данных в распределенной системе (CAP-теорему), не сказал про компенсирующие операции при сбоях. Также не уточнил, как именно кэшировать и инвалидировать.
Мой вариант ответа:
Варианты:
1. Синхронный REST/gRPC – прост, но создает жесткую связанность и зависимость по времени. Используем Circuit Breaker, Retry с экспоненциальной задержкой, таймауты. Компромисс: доступность (availability) жертвуется ради консистентности (consistency).
2. Асинхронный через очередь (RabbitMQ/Kafka) – OrderService подписывается на события обновления пользователей и хранит локальный кэш/материализованный. Компромисс: консистентность (eventual consistency) ради доступности. Нужно обрабатывать возможную рассинхронизацию.
3. API Composition – OrderService делает запрос к API Gateway, который агрегирует данные.
4. CQRS – разделение команд и запросов, отдельная читаемая модель пользователя для сервиса заказов.
Компромиссы: между согласованностью, доступностью, задержкой и сложностью реализации.
—
Вопрос 4
Интервьюер: «Какой из популярных брокеров с вы бы выбрали при построении архитектуры и для каких кейсов какой подойдет лучше?»
Ответ кандидата (суть):
Выбирал бы из трех (RabbitMQ, Kafka, возможно, еще что-то).
Работал с RabbitMQ и c redis, с Kafka не сталкивался.
Kafka хорош для сложной гарантированной доставки, больших потоков, аналитики.
RabbitMQ – компромиссный вариант для простых очередей с низкой задержкой.
Оценка:
Ответ очень общий, без конкретных критериев. Кандидат не назвал различия по протоколам, гарантиям доставки, хранению сообщений, паттернам (work queues, pub/sub, streaming). Не упомянул Redis Streams, AWS SQS. Оценка «компромиссный» не раскрыта.
Мой вариант ответа:
Выбор зависит от кейса:
— RabbitMQ – для классических очередей задач, маршрутизации, RPC, когда важна низкая задержка и сложная маршрутизация. Поддержка транзакций, подтверждений (ack). Хорош для микросервисных команд.
— Apache Kafka – для стриминга больших объемов, логирования, аналитики, event sourcing, когда важна долговременное хранение сообщений, воспроизведение (replay), партиционирование, высокая пропускная способность. Не подходит для однократной доставки с низкой задержкой.
— Redis Streams – для легковесных сценариев с высокой производительностью, но без сложной гарантии.
— AWS SQS/Google PubSub – для управляемых сервисов в облаке.
Критерии: требования к задержке, порядку сообщений, сохранности, масштабируемости, сложности поддержки.
—
Вопрос 5
«Какие критерии вы можете выделить для оценки качества кода и какие принципы разработки используются в вашей команде?»
Ответ кандидата (суть):
Код-ревью, тесты, использование ИИ для ревью, статические анализаторы (лентеры). Принципы: SOLID, DRY, KISS. В команде код не уходит в продакшн без проверки коллегой.
Оценка:
Ответ правильный, но поверхностный. Кандидат не раскрыл критерии конкретно (читаемость, поддерживаемость, производительность, покрытие тестами, отсутствие дублирования, сложность цикломатическая и т.д.). Не упомянул метрики (например, Code Coverage, CRAP-индекс). Упоминание ИИ – современно, но не вместо, а в дополнение.
Мой вариант ответа:
Критерии качества кода:
— Читаемость – понятные имена, комментарии там, где нужны, соблюдение стандартов (PSR в PHP).
— Структурность – следование SOLID, DRY, KISS, YAGNI.
— Тестируемость – наличие юнит- и интеграционных тестов, покрытие критических путей.
— Безопасность – отсутствие уязвимостей (SQL-инъекции, XSS, validation).
— Производительность – отсутствие N+1 запросов, неэффективных алгоритмов.
— Поддерживаемость – низкая связанность, высокая связность внутри модуля.
В команде практикуем: обязательное code review, pre-commit хуки с линтерами (PHP_CodeSniffer, PHPStan, Psalm), CI/CD с прогоном тестов, обсуждение архитектуры до реализации, ретроспективы по качеству.
—
Вопрос 6Ds
«Как понять, что Unitas написан грамотно? Каких принципов тестирования придерживаетесь?» (вероятно, имелись в виду юнит-тесты)
Ответ кандидата (суть):
Тесты должны быть изолированными, воспроизводимыми, быстрыми, с понятными именами, покрывать критические и граничные условия. Покрытие должно быть максимальным. ИИ помогает писать тесты.
Оценка:
Ответ верен, но очень общий. Кандидат не упомянул про принципы FIRST (Fast, Independent, Repeatable, Self-Validating, Timely), про использование моков/стабов, про тестирование исключений, про AAA (Arrange-Act-Assert). «Максимальное покрытие» – это хорошо, но важнее покрытие логики, а не строк.
Мой вариант ответа:
Грамотный юнит-тест:
— Соответствует принципу FIRST.
— Использует паттерн AAA (Arrange-Act-Assert).
— Проверяет как счастливый путь, так и краевые случаи и исключения.
— Не тестирует чужой код (базы данных, внешние API) – для этого есть интеграционные тесты.
— Имеет осмысленное имя, отражающее сценарий.
— Один тест – одна проверка.
Принципы тестирования:
— Тестирование как спецификация – тесты отражают требования.
— Пирамида тестов – много юнит-тестов, меньше интеграционных, еще меньше end-to-end.
— TDD (Test-Driven Development) при разработке сложной логики.
Критично: покрытие не самоцель, важна проверка значимых бизнес-правил.
—
Вопрос 7
Интервьюер: «Есть некоторая библиотека, подключенная через composer. Вам нужно изменить логику 1 публичного метода, в 1 из финальных классов. Как это правильно сделать?»
Ответ кандидата (суть):
Так как класс final, наследование невозможно. Нужно использовать композицию или паттерн декоратор, чтобы обернуть класс и изменить поведение.
Оценка:
Ответ верен. Кандидат правильно указал, что final не наследуется, и предложил композицию/декоратор. Однако не уточнил, что в PHP можно использовать трейты, переопределение через расширение (если не финальный), или патчинг через рантайм (не рекомендуется). Также не сказал про форк библиотеки как крайний вариант.
Мой вариант ответа:
Правильные подходы (по порядку предпочтения):
1. Композиция с паттерном Декоратор – создаем свой класс, который принимает экземпляр исходного класса в конструктор и делегирует вызовы, переопределяя нужный метод.
2. Стратегия – внедрить поведение через интерфейс, если библиотека это поддерживает.
3. Форк библиотеки – изменить код напрямую в своем форке и подключить его через composer (с заменой пакета), но это создает проблемы с обновлениями.
4. Использовать alias в composer для подмены класса (не рекомендуется).
Никогда не следует редактировать файлы библиотеки внутри `vendor/`. Если класс не final, наследование возможно, но его использование тоже должно быть обосновано (принцип предпочитай композицию наследованию).
—
Вопрос 8
Интервьюер: «Если бы вы работали в DDD парадигме, вы бы выбрали богатые или анимичные модели и почему? Какие плюсы и минусы?» (вероятно, «анаемичные»)
Ответ кандидата (суть):
Богатые модели содержат бизнес-логику, анаемичные – только данные (как DTO).
Анаемичные проще для CRUD, понятны новичкам, но логика размазывается по сервисам, дублируется, сложнее поддерживать.
Богатые держат логику в одном месте, улучшают тестируемость, но сложнее, требуют опыта и дисциплины.
Вывод: для сложной логики – богатые, для простой – анаемичные.
Оценка:
Ответ правильный и довольно полный. Кандидат адекватно описал оба подхода и их компромиссы. Cуть передана верно.
Мой вариант ответа:
В DDD предпочтение отдается богатым моделям (Rich Domain Model), так как они инкапсулируют бизнес-правила и обеспечивают целостность данных. Они соответствуют принципу «поведение рядом с данными».
Плюсы богатых моделей: бизнес-логика в одном месте, высокая выразительность, легче тестировать бизнес-правила без сервисов, лучшая поддерживаемость в сложных доменах.
Минусы: сложность, труднее разделять на слои, избыточность для простых CRUD-сценариев.
Анаемичные модели (только свойства и геттеры/сеттеры) – антипаттерн в DDD, но могут использоваться как DTO на границах или в простых модулях. В сложных доменах они приводят к размазыванию логики по сервисам (Transaction Script), нарушению инкапсуляции и дублированию.
В DDD строго рекомендуется Rich Model, но в гибридных системах можно комбинировать: ядро с богатыми моделями, периферия – с анаемичными.
Нет Ответов