Перейти к основному содержимому

РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ PНР разработчик Innowise Group - Middle

· 141 мин. чтения

Сегодня мы разберем собеседование начинающего PHP-разработчика, который прошел путь от стажировки до коммерческого опыта с Symfony и PostgreSQL и сейчас ищет свой следующий проект. Интервью показывает, как кандидат уверенно ориентируется в базовых концепциях HTTP, ООП, SQL и докера, но местами теряется в деталях и практических примерах, что особенно заметно на архитектурных и английских вопросах. Этот разбор будет полезен джунам и мидлам как иллюстрация типичных сильных и слабых сторон на техническом интервью.

Вопрос 1. Кратко расскажи о своём образовании и опыте коммерческой разработки.

Таймкод: 00:01:15

Ответ собеседника: правильный. Кратко описывает профильное образование, стажировку и опыт работы над системой электронного документооборота на Symfony и PostgreSQL, с указанием причины завершения работы (сокращение).

Правильный ответ:

Краткий и уверенный ответ на такой вопрос должен показать:

  • профильное или смежное образование;
  • релевантный технологический стек;
  • реальный практический опыт и вклад в продукты;
  • связь предыдущего опыта с текущей позицией (в том числе переход в Go).

Пример сильного ответа:

Я получил профильное образование в области программирования в Белорусской государственной академии связи, где изучал основы алгоритмов, сетей, баз данных и промышленной разработки ПО. Во время учёбы прошёл стажировку в Itransition, где получил первый опыт работы в командной среде: code review, Git-flow, работа по задачам в трекере, базовые практики CI и написание юнит-тестов.

Затем работал в юридической компании над системой электронного документооборота. Мы развивали веб-приложение на Symfony с PostgreSQL в качестве основной СУБД. Я участвовал в:

  • проектировании структуры БД (нормализация, индексы, внешние ключи, оптимизация запросов);
  • реализации бизнес-логики (маршруты согласования документов, статусы, роли, права доступа);
  • интеграции с внешними сервисами и внутренними системами;
  • оптимизации производительности запросов и части тяжёлых операций (batch-обновления, поиск по документам);
  • поддержании качества кода: рефакторинг, покрытие тестами ключевых модулей, участие в code review.

Этот опыт дал мне понимание того, как строятся корпоративные системы: доменная модель, транзакционность, миграции БД, обработка ошибок, логирование, деплой и сопровождение. Переход в Go для меня логичен: я уже работал с backend-архитектурой, понимаю требования к надёжности и производительности, и этот опыт напрямую переносится на разработку сервисов на Go (REST/gRPC API, работа с PostgreSQL, микросервисы, очереди, транзакции, тестирование, CI/CD).

Вопрос 2. Почему ты выбрал для разработки именно PHP?

Таймкод: 00:02:17

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

Правильный ответ:

Разумный ответ должен отражать не только «простоту входа», но и осознанный выбор с учётом экосистемы, задач и дальнейшей эволюции в более строгие и производительные стэки (включая Go).

Короткий, но содержательный вариант:

Я выбрал PHP не только из-за низкого порога входа, но и потому, что на момент старта это был один из самых практичных языков для быстрой коммерческой разработки веб-сервисов. Он давал:

  • развитую экосистему: фреймворки (Symfony, Laravel), огромное количество библиотек и решений;
  • быстрый путь от идеи до рабочего прототипа и продакшена;
  • широкое применение в бизнесе: CRM, порталы, внутренние системы, публичные сайты.

Работая с PHP, я сосредоточился не на «скриптовости», а на инженерных практиках:

  • применял ООП, паттерны проектирования, SOLID;
  • использовал современные фреймворки (например, Symfony), DI-контейнеры, middleware, конфигурацию через environment;
  • проектировал и оптимизировал работу с PostgreSQL: схемы БД, индексы, транзакции, сложные запросы;
  • строил модульную архитектуру, интегрировал внешние сервисы, внедрял логирование, тестирование и CI/CD.

Этот опыт стал хорошим фундаментом для перехода к более жёстко типизированным и высокопроизводительным решениям, таким как Go:

  • понимание HTTP, REST, авторизации/аутентификации, транзакций, миграций, очередей;
  • опыт построения устойчивых backend-сервисов;
  • переносимые практики: чистая архитектура, разделение слоёв (transport, business, storage), тестируемость.

То есть выбор PHP был прагматичным стартом в реальной веб-разработке, а не случайным шагом, и далее логично эволюционировал в сторону более строгих и производительных технологий.

Вопрос 3. Кратко расскажи, чем занималась система твоего последнего проекта и какую роль ты в ней выполнял.

Таймкод: 00:02:48

Ответ собеседника: неполный. Указывает, что работал над системой документооборота на PHP/Symfony, выполнял задачи от тимлида, но не раскрывает конкретные зоны ответственности, функциональность и влияние.

Правильный ответ:

Для сильного ответа важно показать:

  • назначение системы;
  • ключевую бизнес-логику;
  • архитектурные и технические решения;
  • свою конкретную роль, ответственность и вклад;
  • опыт, который переносится на Go/modern backend.

Пример развернутого ответа:

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

Ключевые функции системы включали:

  • управление документами:
    • создание документов на основе шаблонов,
    • версионирование,
    • отслеживание истории изменений;
  • маршрутизация и согласование:
    • настраиваемые маршруты согласования,
    • роли и уровни доступа (юристы, партнёры, клиенты),
    • статусы документов (черновик, на согласовании, подписан, отклонён);
  • работа с клиентами:
    • привязка документов к контрагентам и делам,
    • поиск по реквизитам, номеру договора, контрагенту, дате, статусу;
  • безопасность и аудируемость:
    • логирование всех действий,
    • контроль прав доступа на уровне ролей и объектов,
    • подготовка данных для внутреннего аудита и проверок.

Технически:

  • backend: PHP, Symfony;
  • БД: PostgreSQL (индексы, внешние ключи, транзакции, миграции);
  • интеграции: внутренние сервисы компании, почта, возможно модули для ЭЦП/подписания документов;
  • стандарты: REST API, авторизация, раздельные слои приложения.

Моя роль и зона ответственности:

  • участвовал в реализации модулей бизнес-логики:
    • маршруты согласования;
    • статусы документов;
    • роли и ACL-проверки доступа;
    • уведомления о смене статуса документа;
  • работал с PostgreSQL:
    • писал SQL-запросы (включая join-ы, фильтрацию, пагинацию);
    • помогал оптимизировать запросы, добавлял индексы под частые выборки;
    • участвовал в подготовке миграций;
  • занимался интеграцией:
    • подключение к внутренним сервисам компании (например, данные о клиентах);
    • работа с API сторонних сервисов, если это требовалось;
  • качество и поддержка:
    • фиксировал баги в продакшене, искал причины по логам и данным,
    • писал и дорабатывал модульные/интеграционные тесты для критичных участков,
    • участвовал в code review под руководством более опытных разработчиков;
  • взаимодействие с командой:
    • работал по задачам из трекера, уточнял требования у аналитика/тимлида,
    • давал оценки задачам, участвовал в обсуждении подходов при реализации.

Такой опыт показывает:

  • умение работать с реальной бизнес-доменной моделью, а не просто CRUD;
  • понимание надежности, транзакционности и консистентности данных;
  • практику работы со сложной авторизацией, статусами и жизненным циклом сущностей;
  • навыки, напрямую применимые при разработке сервисов на Go: проектирование API, работа с PostgreSQL, логирование, разбор инцидентов, чтение и поддержка чужого кода, следование архитектурным и командным практикам.

Вопрос 4. Что такое HTTP и как он используется при взаимодействии клиента и сервера?

Таймкод: 00:03:28

Ответ собеседника: правильный. Описывает HTTP как протокол прикладного уровня, по которому клиент отправляет запросы серверу и получает ответы.

Правильный ответ:

HTTP — это протокол прикладного уровня, предназначенный для обмена данными между клиентом и сервером, в первую очередь в вебе. Он определяет:

  • формат запросов и ответов;
  • способы идентификации ресурса (URL/URI);
  • методы работы с ресурсами (GET, POST, PUT, DELETE и др.);
  • механизмы передачи метаданных (заголовки);
  • коды состояния, описывающие результат обработки запроса.

Ключевые идеи HTTP:

  1. Клиент-серверная модель

    • Клиент (обычно браузер или другое приложение) формирует HTTP-запрос.
    • Сервер принимает запрос, обрабатывает его (бизнес-логика, БД, кэш, внешние сервисы) и возвращает HTTP-ответ.
    • HTTP сам по себе не описывает бизнес-логику, он задаёт стандартизированную "оболочку" взаимодействия.
  2. Методы запросов
    Основные методы (важно понимать их семантику, особенно при проектировании API):

    • GET — получить представление ресурса. Должен быть безопасным (не менять состояние) и идемпотентным.
    • POST — создать ресурс или выполнить операцию, изменяющую состояние. Не идемпотентен.
    • PUT — полная замена ресурса по заданному идентификатору. Идемпотентен.
    • PATCH — частичное изменение ресурса.
    • DELETE — удаление ресурса. Идемпотентен по семантике.
    • Важно: идемпотентность и безопасность нужны для корректной работы ретраев, кешей, прокси, балансировщиков.
  3. Структура HTTP-запроса

    • Стартовая строка: метод, путь, версия протокола
      Пример: GET /users/123 HTTP/1.1
    • Заголовки (Headers): метаданные (авторизация, тип содержимого, кэширование и др.)
    • Тело (Body): опционально, для передачи данных (JSON, формы, файлы и т.д.)
  4. Структура HTTP-ответа

    • Стартовая строка: версия протокола + код статуса + текст
      Пример: HTTP/1.1 200 OK
    • Заголовки (Content-Type, Content-Length, Cache-Control, Set-Cookie и др.)
    • Тело: данные ответа (HTML, JSON, бинарные данные и т.д.)
  5. Коды состояния (status codes)
    Важные группы:

    • 2xx — успех (200 OK, 201 Created, 204 No Content)
    • 3xx — редиректы (301, 302, 304 Not Modified)
    • 4xx — ошибка клиента (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity)
    • 5xx — ошибка сервера (500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable) Корректный выбор кодов критичен для хороших API.
  6. Без состояния (stateless)

    • Каждый запрос должен содержать всю информацию, необходимую для обработки (token/session id/headers).
    • Сервер не обязан хранить контекст между запросами (хотя может использовать сессии или токены, но это уже уровень приложения).
    • Это упрощает масштабирование: любой экземпляр сервера может обработать любой запрос.
  7. Работа поверх TCP/TLS

    • HTTP/1.1 работает поверх TCP; поддерживает keep-alive (несколько запросов по одному соединению).
    • HTTPS — HTTP поверх TLS (шифрование, целостность, аутентичность).
    • HTTP/2 и HTTP/3 добавляют мультиплексирование, более эффективное использование соединений и улучшения производительности.
    • Для разработчика backend-сервисов важно понимать влияние версий HTTP на latency, количество соединений и работу балансировщиков.
  8. Использование в веб- и backend-разработке (в том числе на Go)
    В типичном backend-сервисе:

    • HTTP используется как основной транспорт для:
      • REST API
      • GraphQL
      • webhooks
      • интеграций между сервисами
    • Важно уметь:
      • правильно моделировать ресурсы и URIs;
      • выбирать семантически корректные методы;
      • работать с заголовками (Accept, Content-Type, Authorization, X-Request-ID, Cache-Control и др.);
      • возвращать осмысленные коды ответов и ошибки.

Пример простого HTTP-сервера на Go:

package main

import (
"encoding/json"
"log"
"net/http"
)

type User struct {
ID int `json:"id"`
Name string `json:"name"`
}

func userHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}

// Пример: получение пользователя (обычно был бы доступ к БД)
user := User{ID: 1, Name: "Alice"}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

if err := json.NewEncoder(w).Encode(user); err != nil {
log.Printf("encode error: %v", err)
}
}

func main() {
http.HandleFunc("/user", userHandler)

log.Println("listening on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}

В этом примере видно классическую модель:

  • клиент отправляет GET /user HTTP/1.1;
  • сервер обрабатывает запрос и возвращает 200 OK с JSON;
  • разработчик контролирует метод, код ответа, заголовки и тело — то есть полную реализацию HTTP-контракта.

Вопрос 5. Какие HTTP-методы ты знаешь и какие из них использовал на практике?

Таймкод: 00:04:03

Ответ собеседника: правильный. Перечисляет GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD; отмечает, что в основном работал с GET и POST.

Правильный ответ:

Важно не только перечислить методы, но и понимать их семантику, идемпотентность, типичные кейсы использования и влияние на проектирование API. Это критично при разработке backend-сервисов, интеграций и микросервисной архитектуры.

Основные методы:

  1. GET

    • Назначение: получение представления ресурса.
    • Характеристики:
      • безопасный (не изменяет состояние на сервере);
      • идемпотентный;
      • данные передаются через URL (query-параметры).
    • Примеры:
      • GET /users — список пользователей;
      • GET /users/123 — детали конкретного пользователя.
  2. POST

    • Назначение:
      • создание ресурса;
      • выполнение нестандартных операций, которые изменяют состояние (логин, обработка команд).
    • Характеристики:
      • не идемпотентен (повтор запроса может создать несколько ресурсов).
    • Примеры:
      • POST /users — создание пользователя;
      • POST /documents/123/approve — выполнить доменное действие над документом.
  3. PUT

    • Назначение: полная замена ресурса по известному идентификатору.
    • Характеристики:
      • идемпотентен: повторный запрос с теми же данными не меняет результат.
    • Примеры:
      • PUT /users/123 — полностью заменить данные пользователя 123.
    • Типичная ошибка:
      • использовать PUT как «частичное обновление». Для этого лучше PATCH.
  4. PATCH

    • Назначение: частичное обновление ресурса.
    • Характеристики:
      • не гарантированно идемпотентен по стандарту, но часто реализуется как идемпотентный;
      • передаёт только изменяемые поля.
    • Примеры:
      • PATCH /users/123 с телом {"email": "new@mail.com"}.
  5. DELETE

    • Назначение: удаление ресурса.
    • Характеристики:
      • семантически идемпотентен (повторный DELETE того же ресурса должен давать тот же итоговый результат — ресурса нет).
    • Примеры:
      • DELETE /users/123.
  6. HEAD

    • Назначение: получить только заголовки ответа, без тела.
    • Использование:
      • проверка доступности ресурса;
      • проверка метаданных (Content-Length, ETag) для кеширования.
    • Пример:
      • HEAD /files/report.pdf — узнать, существует ли файл и его размер.
  7. OPTIONS

    • Назначение: узнать, какие методы и параметры поддерживает ресурс или сервер.
    • Часто используется:
      • для CORS-предзапросов (preflight) в браузере.
    • Примеры:
      • OPTIONS /usersAllow: GET, POST, OPTIONS.

Ключевые концепции, которые важно уметь объяснить:

  • Безопасность (safe methods):
    • GET и HEAD не должны изменять состояние.
  • Идемпотентность:
    • GET, PUT, DELETE, HEAD, OPTIONS — идемпотентные;
    • POST — нет;
    • PATCH — зависит от реализации.
  • Практическое значение идемпотентности:
    • корректная работа ретраев, балансировщиков, прокси;
    • предсказуемость поведения API при сетевых сбоях.

Пример корректного использования методов в REST-подобном API для системы документооборота:

  • GET /documents — получить список документов (фильтрация через query params).
  • POST /documents — создать новый документ.
  • GET /documents/{id} — получить документ по id.
  • PUT /documents/{id} — заменить документ целиком (например, все метаданные).
  • PATCH /documents/{id} — изменить часть полей (статус, заголовок).
  • DELETE /documents/{id} — пометить документ как удалённый или удалить.
  • POST /documents/{id}/approve — доменное действие (утверждение документа).

Пример минимального HTTP-обработчика на Go с разными методами:

func documentHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// Получение документа
// GET /document?id=123
case http.MethodPost:
// Создание документа
case http.MethodPut:
// Полное обновление документа
case http.MethodPatch:
// Частичное обновление документа
case http.MethodDelete:
// Удаление документа
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}

Осознанный выбор HTTP-методов, соответствующий семантике операций, — фундамент качественного API-дизайна и профессиональной серверной разработки.

Вопрос 6. Что такое идемпотентные HTTP-методы и какие методы к ним относятся?

Таймкод: 00:04:27

Ответ собеседника: неполный. Верно указывает, что идемпотентные методы при повторных вызовах дают один и тот же результат, считает идемпотентными все методы, кроме POST и частично PATCH, но без уверенной и чёткой формулировки.

Правильный ответ:

Идемпотентность HTTP-метода — это свойство, при котором многократное выполнение одного и того же запроса с одинаковыми параметрами приводит к одному и тому же конечному состоянию ресурса на сервере. Результат (состояние данных) после первого вызова и после N-го вызова должен быть одинаковым, даже если ответы (например, заголовки или время) могут немного отличаться.

Это свойство важно для:

  • безопасности ретраев (повторных запросов при сетевых сбоях);
  • корректной работы прокси, балансировщиков, gateway-ов;
  • предсказуемости поведения API.

К идемпотентным методам по спецификации HTTP относятся:

  • GET
  • HEAD
  • PUT
  • DELETE
  • OPTIONS
  • TRACE

POST — не идемпотентен.
PATCH — по стандарту не гарантированно идемпотентен, его идемпотентность зависит от конкретной реализации.

Разбор по методам:

  1. GET (идемпотентный, безопасный)
  • Не должен изменять состояние на сервере.
  • Повторный вызов GET /users/123 только читает данные.
  • Даже если логируется просмотр, это считается побочным эффектом, а не изменением доменного состояния ресурса.
  1. HEAD (идемпотентный, безопасный)
  • Как GET, но без тела.
  • Используется для проверки доступности ресурса и метаданных.
  1. PUT (идемпотентный)
  • Полная замена ресурса.
  • Пример:
    • Первый запрос:
      • PUT /users/123 с телом {"name":"Alice"} — создаёт или заменяет ресурс.
    • Последующие идентичные PUT-запросы приводят к тому же состоянию (пользователь 123 с name="Alice").
  • Важно при реализации: не превращать PUT в "добавить ещё один", иначе теряется идемпотентность.
  1. DELETE (идемпотентный)
  • Удаление ресурса.
  • Первый DELETE /users/123 удаляет пользователя.
  • Повторный DELETE того же ресурса:
    • либо возвращает 404/410,
    • либо 204,
    • но состояние остаётся "ресурса нет" — то есть идемпотентность соблюдается.
  • Ошибка — если повторный DELETE вдруг создаёт что-то или изменяет состояние ещё как-то неожиданно.
  1. OPTIONS (идемпотентный)
  • Запрос возможностей ресурса (какие методы поддерживаются и т.п.).
  • Повторные вызовы не меняют состояние.
  1. TRACE (идемпотентный, редко используется)
  • Диагностический метод, возвращает запрос как есть.
  1. POST (не идемпотентный)
  • Обычно создаёт новые ресурсы или выполняет операции, которые изменяют состояние (платёж, создание заказа).
  • Два одинаковых POST-запроса часто создают два разных ресурса или две операции.
  • Поэтому при проектировании платежей, команд и иных операций:
    • либо осознанно принимаем неидемпотентность,
    • либо добавляем idempotency key на прикладном уровне.
  1. PATCH (зависит от реализации)
  • По стандарту может быть неидемпотентным.
  • Пример неидемпотентного PATCH:
    • PATCH /counter { "op": "increment", "value": 1 }
      • каждый вызов увеличивает значение.
  • Пример идемпотентного PATCH:
    • PATCH /user/123 { "email": "a@b.com" }
      • повторные вызовы приводят к одному и тому же состоянию.
  • При проектировании API для критичных операций PATCH лучше делать идемпотентным по контракту или использовать PUT.

Практический смысл для backend-разработчика:

  • Балансировщики, прокси, retry-механизмы (в том числе в gRPC/HTTP-клиентах) могут автоматически повторять идемпотентные запросы.
  • Если метод помечен или задуман как идемпотентный, реализация на сервере обязана этому соответствовать.
  • Для потенциально опасных операций (платежи, создание сущностей) часто:
    • Используют POST + idempotency key.
    • Или формируют контракт так, чтобы операция могла быть безопасно повторена.

Пример на Go: обработка идемпотентного PUT

func updateUserHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}

id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}

var req struct {
Name string `json:"name"`
Email string `json:"email"`
}

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}

// Идемпотентная логика:
// - либо создаём/обновляем пользователя с конкретным id
// - повторный вызов с теми же данными оставит состояние таким же

// Пример (псевдокод):
// UPSERT INTO users (id, name, email) VALUES ($1, $2, $3)
// ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, email = EXCLUDED.email;

// ...
w.WriteHeader(http.StatusNoContent)
}

Пример SQL для идемпотентного PUT (PostgreSQL):

INSERT INTO users (id, name, email)
VALUES ($1, $2, $3)
ON CONFLICT (id)
DO UPDATE SET
name = EXCLUDED.name,
email = EXCLUDED.email;

Такое решение гарантирует, что один и тот же PUT-запрос приведёт к одному и тому же конечному состоянию, как и требует идемпотентность.

Вопрос 7. Работал ли ты с REST и что это такое?

Таймкод: 00:04:57

Ответ собеседника: неправильный. Говорит, что REST — это набор правил для удобного интерфейса поверх HTTP с JSON. Это частично искажает суть REST: REST — архитектурный стиль, не привязанный жестко к JSON и не сводящийся только к "удобному HTTP API".

Правильный ответ:

REST (Representational State Transfer) — это архитектурный стиль взаимодействия распределённых систем, описанный Роем Филдингом. Это не фреймворк, не стандарт и не "просто JSON поверх HTTP". REST задаёт набор принципов, которые позволяют строить простые, масштабируемые, кэшируемые и легко эволюционирующие веб-сервисы.

Ключевые идеи REST:

  1. Клиент-серверная архитектура

    • Клиент (frontend, мобильное приложение, другой сервис) и сервер чётко разделены:
      • Клиент отвечает за интерфейс и взаимодействие с пользователем.
      • Сервер отвечает за хранение данных и реализацию бизнес-логики.
    • Это упрощает развитие каждого компонента независимо.
  2. Отсутствие состояния на сервере (stateless)

    • Каждый запрос содержит всю необходимую информацию для его обработки: токен/куки, параметры, контекст.
    • Сервер не хранит "сеансовое состояние" между запросами клиента (если нужно состояние, его кодируют в токенах, ключах сессий, или хранят в внешних хранилищах).
    • Это:
      • упрощает горизонтальное масштабирование;
      • позволяет любому экземпляру сервиса обработать любой запрос.
  3. Единый интерфейс (uniform interface) Это фундаментальное требование REST. Важные аспекты:

    • Ресурсо-ориентированность:
      • Мы мыслим не "методами", а "ресурсами": users, orders, documents.
      • Каждый ресурс имеет уникальный URI:
        • /users, /users/123, /documents/42/versions.
    • Использование стандартных HTTP-методов с корректной семантикой:
      • GET — чтение, POST — создание/команда, PUT — полная замена, PATCH — частичное обновление, DELETE — удаление.
    • Самодокументируемость через структурированные ответы и понятные коды статусов.
    • В идеале — HATEOAS (Hypermedia As The Engine Of Application State):
      • Сервер в ответе даёт ссылки на связанные действия и ресурсы.
      • На практике редко реализуется полноценно, но понимание принципа плюс.
  4. Кэшируемость

    • Ответы могут маркироваться как кэшируемые (Cache-Control, ETag, Last-Modified).
    • Клиент/прокси может использовать кэшированные ответы для снижения нагрузки и ускорения.
    • Разработчик должен осознанно выбирать, что можно кэшировать, а что — нет.
  5. Слоями (layered system)

    • Между клиентом и сервером могут быть прокси, балансировщики, кэши, API-шлюзы.
    • Клиент не должен знать, есть ли между ним и конечным сервером промежуточный слой.
    • Это позволяет строить сложную инфраструктуру прозрачно.
  6. Представления ресурса (Representations)

    • Ресурс — абстрактная сущность (пользователь, документ, заказ).
    • Представление — конкретный формат данных:
      • JSON, XML, HTML, CSV, protobuf и т.д.
    • REST НЕ привязан к JSON. JSON стал де-факто стандартом для веб-API, но это лишь один из форматов.
    • Клиент и сервер могут использовать content negotiation:
      • Accept: application/json
      • Content-Type: application/json
  7. REST vs "просто HTTP+JSON"

    • "HTTP + JSON" ещё не делает API RESTful.
    • Частые ошибки:
      • RPC-стиль: /doAction с POST вместо ресурсного /orders/{id}/cancel.
      • Игнорирование семантики методов (всё через POST).
      • Неправильные коды статуса (всегда 200, ошибки — только в теле).
    • REST по сути про:
      • чёткую модель ресурсов;
      • корректное использование HTTP как протокола (методы, коды, заголовки);
      • простоту, предсказуемость и расширяемость.

Примеры REST-подхода для системы документооборота:

  • Получить список документов:
    • GET /documents?status=pending&limit=20
  • Создать документ:
    • POST /documents
    • Тело (JSON):
      {
      "title": "Договор №123",
      "client_id": 42,
      "content": "..."
      }
    • Ответ: 201 Created + Location: /documents/101
  • Получить документ:
    • GET /documents/101
  • Обновить метаданные документа:
    • PATCH /documents/101
      { "title": "Договор №123 (ред.)" }
  • Утвердить документ:
    • семантически корректнее доменное действие:
      • POST /documents/101/approve
      • или PATCH /documents/101 со статусом approved
    • так сохраняется явная модель домена.

Пример простого REST-like обработчика на Go:

package main

import (
"encoding/json"
"log"
"net/http"
"strings"
)

type Document struct {
ID int `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
}

func documentHandler(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/documents")
w.Header().Set("Content-Type", "application/json")

switch {
// GET /documents
case r.Method == http.MethodGet && path == "":
listDocuments(w, r)
// GET /documents/{id}
case r.Method == http.MethodGet && path != "":
getDocument(w, r, strings.TrimPrefix(path, "/"))
// POST /documents
case r.Method == http.MethodPost && path == "":
createDocument(w, r)
default:
http.Error(w, "not found", http.StatusNotFound)
}
}

func listDocuments(w http.ResponseWriter, r *http.Request) {
docs := []Document{
{ID: 1, Title: "Contract 1", Status: "pending"},
{ID: 2, Title: "Contract 2", Status: "approved"},
}
json.NewEncoder(w).Encode(docs)
}

func getDocument(w http.ResponseWriter, r *http.Request, id string) {
// В реальности здесь парсим id, идём в БД, обрабатываем ошибки.
doc := Document{ID: 1, Title: "Contract 1", Status: "pending"}
json.NewEncoder(w).Encode(doc)
}

func createDocument(w http.ResponseWriter, r *http.Request) {
var req Document
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
// В реальности: валидация + вставка в БД.
req.ID = 3
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(req)
}

func main() {
http.HandleFunc("/documents", documentHandler)
http.HandleFunc("/documents/", documentHandler)

log.Println("listening on :8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}

Кратко: REST — это архитектурный стиль, основанный на ресурсах, правильной семантике HTTP, stateless-взаимодействии и кэшируемости. Формат JSON — лишь удобное представление, а не определяющий признак REST. Такой уровень понимания ожидается при работе с современными HTTP/REST API.

Вопрос 8. Что такое SOAP и чем он отличается по форме данных и протоколам?

Таймкод: 00:05:45

Ответ собеседника: неполный. Знает о SOAP, говорит, что там вместо JSON используется HTML (затем поправляется на XML) и что он менее привязан к HTTP, но объяснение фрагментарное и неточное.

Правильный ответ:

SOAP (Simple Object Access Protocol) — это протокол обмена структурированными сообщениями, изначально разработанный для интеграции распределённых систем. В отличие от REST (который является архитектурным стилем), SOAP — формализованный протокол с чёткой спецификацией формата сообщений, ошибок, расширений и контрактов.

Ключевые особенности SOAP:

  1. Формат данных: XML
  • Все сообщения SOAP представлены в формате XML.
  • Сообщение SOAP — это XML-конверт (envelope) со строгой структурой:
    • Envelope — корневой элемент.
    • Header — метаданные (аутентификация, транзакции, маршрутизация и т.п.).
    • Body — полезная нагрузка (вызов операции или ответ).
  • JSON, HTML и другие форматы не являются стандартным форматом SOAP-сообщений.

Пример простого SOAP-запроса:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ex="http://example.com/">
<soapenv:Header/>
<soapenv:Body>
<ex:GetUserRequest>
<ex:UserId>123</ex:UserId>
</ex:GetUserRequest>
</soapenv:Body>
</soapenv:Envelope>
  1. Контрактно-ориентированность (WSDL)
  • SOAP-сервисы обычно описываются через WSDL (Web Services Description Language).
  • WSDL — это XML-документ, который:
    • описывает операции сервиса (методы),
    • входные и выходные сообщения,
    • типы данных (часто через XSD),
    • используемые протоколы и точки доступа (endpoints).
  • Клиенты часто генерируются автоматически по WSDL:
    • строго типизированный контракт,
    • меньше места для неоднозначностей, но выше связанность.
  1. Транспортный уровень
  • SOAP не привязан только к HTTP:
    • может работать поверх HTTP, SMTP, TCP, JMS и других транспортов.
  • На практике чаще всего SOAP поверх HTTP(S), но:
    • в отличие от REST, HTTP используется как транспорт, а не как семантическая часть протокола.
    • HTTP-методы (обычно POST) не несут бизнес-семантики, вся логика зашита внутрь XML-сообщения.
  1. Стиль взаимодействия
  • Обычно RPC-подобный стиль:
    • операции выглядят как вызовы методов: GetUser, CreateOrder, ProcessPayment.
    • URL один (endpoint), а действие определяется содержимым SOAP Body и/или заголовками.
  • В отличие от ресурсно-ориентированного REST:
    • SOAP меньше про ресурсы и URI, больше про операции и контракты.
  1. Стандарты и расширения (WS-) SOAP-среда активно использует стек WS-:
  • WS-Security — подпись, шифрование, токены, сложные схемы безопасности.
  • WS-ReliableMessaging — гарантированная доставка, порядок сообщений.
  • WS-Addressing — адресация сообщений.
  • WS-Transaction — распределённые транзакции. Это делает SOAP:
  • подходящим для enterprise-интеграций с формальными требованиями,
  • тяжеловесным по сравнению с легковесными REST/JSON/gRPC решениями.
  1. Отличия SOAP от REST/HTTP+JSON (по сути вопроса):
  • Формат данных:
    • SOAP: строго XML (SOAP Envelope с определённой схемой).
    • REST: любой формат (JSON/XML/HTML/CSV/protobuf), JSON часто де-факто стандарт.
  • Использование HTTP:
    • SOAP:
      • HTTP чаще всего только транспорт;
      • обычно один endpoint и POST-запросы;
      • семантика HTTP-методов почти не используется.
    • REST:
      • опирается на семантику HTTP (GET/POST/PUT/DELETE/PATCH, коды ответов, заголовки).
  • Контракт:
    • SOAP: строгий контракт через WSDL, генерация клиентов.
    • REST: контракт обычно через документацию (OpenAPI/Swagger), но формализация мягче.
  • Расширения:
    • SOAP: развитые стандарты для безопасности, транзакций, надежных сообщений.
    • REST: более легковесные подходы (OAuth2/OIDC/JWT, idempotency keys, retry patterns и т.п.).
  • Сложность:
    • SOAP тяжелее по форме (XML, WS-*), но даёт формальные гарантии и стандартизованные механизмы.
    • REST легче в реализации и потреблении, лучше подходит для веб- и микросервисных API.
  1. Практические моменты для backend-разработчика:
  • Нужно уметь:
    • интегрироваться с legacy/SOAP-сервисами;
    • разбирать SOAP-сообщения;
    • маппить SOAP-контракты на внутренние модели и HTTP/REST/gRPC API.
  • В Go чаще не поднимают новые SOAP-сервисы, а пишут адаптеры к существующим.

Пример концептуальной интеграции в Go (упрощённо, без библиотеки):

import (
"bytes"
"net/http"
)

func callSOAPExample() error {
soapBody := []byte(`
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ex="http://example.com/">
<soapenv:Header/>
<soapenv:Body>
<ex:GetUserRequest>
<ex:UserId>123</ex:UserId>
</ex:GetUserRequest>
</soapenv:Body>
</soapenv:Envelope>`)

req, err := http.NewRequest("POST", "https://legacy.example.com/soap", bytes.NewReader(soapBody))
if err != nil {
return err
}
req.Header.Set("Content-Type", "text/xml; charset=utf-8")
// Часто требуется SOAPAction:
req.Header.Set("SOAPAction", "GetUser")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

// Далее парсим XML-ответ, маппим в наши структуры.
return nil
}

Кратко:

  • SOAP — протокол с формализованным XML-форматом и контрактами (WSDL), способный работать поверх разных транспортов.
  • REST — архитектурный стиль, обычно поверх HTTP, с опорой на ресурсы, методы и коды статусов.
  • Главное отличие по вопросу:
    • форма данных: SOAP → XML (envelope), REST API обычно → JSON;
    • протокол/транспорт: SOAP может работать не только поверх HTTP и использует HTTP как транспорт, а не как часть модели.

Вопрос 9. Что такое ООП и на каких основных принципах оно основано?

Таймкод: 00:06:09

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

Правильный ответ:

Объектно-ориентированное программирование (ООП) — это парадигма, в которой система моделируется как набор взаимодействующих объектов. Каждый объект объединяет состояние (данные) и поведение (методы), соответствуя сущностям предметной области.

Классический набор принципов ООП:

  • инкапсуляция;
  • абстракция;
  • наследование;
  • полиморфизм.

Важно понимать их смысл глубже, чем просто перечисление.

Инкапсуляция

Суть:

  • сокрытие внутреннего состояния и реализации за чётко определённым интерфейсом;
  • контроль доступа к данным через методы/функции, а не прямую модификацию извне;
  • уменьшение связности, повышение защищённости инвариантов.

Практический смысл:

  • можно изменять внутреннюю реализацию без ломки внешнего кода;
  • легче обеспечивать корректность и валидность состояния.

Пример (Go-стиль инкапсуляции):

type Document struct {
id int // неэкспортируемое поле
title string // неэкспортируемое поле
status string
}

// Конструктор
func NewDocument(id int, title string) *Document {
return &Document{
id: id,
title: title,
status: "draft",
}
}

// Экспортируемые методы-интерфейс
func (d *Document) ID() int {
return d.id
}

func (d *Document) Title() string {
return d.title
}

func (d *Document) SetTitle(title string) {
if title == "" {
return // можно кидать ошибку
}
d.title = title
}

Здесь инварианты контролируются методами, а не прямым доступом к полям.

Абстракция

Суть:

  • выделение существенных характеристик сущности и сокрытие несущественных деталей;
  • работа с концепциями (интерфейсами, контрактами), а не с конкретными реализациями.

Практический смысл:

  • код опирается на "что делает" (контракт), а не "как сделано";
  • упрощает поддержку, замену или добавление реализаций.

Пример интерфейса в Go:

type DocumentStorage interface {
Save(doc *Document) error
GetByID(id int) (*Document, error)
}

type PostgresDocumentStorage struct {
// db *sql.DB
}

func (s *PostgresDocumentStorage) Save(doc *Document) error {
// реализация INSERT/UPDATE в PostgreSQL
return nil
}

func (s *PostgresDocumentStorage) GetByID(id int) (*Document, error) {
// реализация SELECT
return &Document{}, nil
}

Код бизнес-логики работает с DocumentStorage, не зная о конкретной БД.

Наследование

Классическая идея:

  • один класс может "наследовать" поля и методы другого, расширяя или изменяя поведение.
  • позволяет переиспользовать код базового класса.

Проблемы классического наследования:

  • усиливает связность;
  • приводит к хрупким иерархиям;
  • часто используется там, где лучше подошла бы композиция.

Современный подход:

  • минимизировать глубокие иерархии;
  • предпочитать композицию и интерфейсы.

В Go прямого классического наследования нет. Вместо него:

  • встраивание (embedding) и композиция.

Пример (embedding вместо наследования):

type BaseDocument struct {
ID int
Title string
}

type SignedDocument struct {
BaseDocument // встраивание
SignedBy string
}

Полиморфизм

Суть:

  • способность работать с объектами разных типов через единый интерфейс, вызывая "правильное" поведение в зависимости от конкретной реализации.

Практический смысл:

  • позволяет заменять реализации без изменения кода, который их использует;
  • ключевой инструмент для расширяемости.

В Go полиморфизм достигается через интерфейсы:

type Notifier interface {
Notify(message string) error
}

type EmailNotifier struct{}
func (n *EmailNotifier) Notify(message string) error {
// отправка email
return nil
}

type SlackNotifier struct{}
func (n *SlackNotifier) Notify(message string) error {
// отправка в Slack
return nil
}

func SendAlert(n Notifier, msg string) {
_ = n.Notify(msg)
}

SendAlert не знает и не зависит от конкретного типа — это и есть полиморфизм.

Почему это важно для современного backend-а и Go

  • Несмотря на то, что Go не является "классическим ООП-языком" с наследованием, он активно использует:
    • инкапсуляцию (экспорт/неэкспорт, методы на типах);
    • абстракцию и полиморфизм через интерфейсы;
    • композицию вместо наследования.
  • Понимание ООП-принципов позволяет:
    • строить устойчивые к изменениям API;
    • выделять доменные модели и слои приложения;
    • избегать "god-объектов" и чрезмерных связей;
    • осмысленно использовать интерфейсы, а не "пихать их везде".

Кратко: ООП — это про моделирование домена через объекты и чёткие контракты. Инкапсуляция, абстракция, полиморфизм и аккуратное (или заменённое композицией) наследование — инструменты для снижения связности, повышения читаемости, расширяемости и тестируемости системы. Именно их зрелое применение отличает качественную серверную архитектуру.

Вопрос 10. Что такое полиморфизм в объектно-ориентированном программировании?

Таймкод: 00:06:28

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

Правильный ответ:

Полиморфизм — это способность кода работать с объектами разных конкретных типов через единый абстрактный интерфейс, при этом вызывать корректное поведение в зависимости от реального типа объекта.

Ключевая идея: вызывающий код опирается на контракт (интерфейс), а не на конкретную реализацию. Это позволяет подменять реализации, расширять систему и тестировать без переписывания логики.

Практический смысл полиморфизма:

  • уменьшается связность между компонентами;
  • можно подставлять разные реализации (prod, mock, in-memory, external API) без изменения бизнес-кода;
  • код становится расширяемым: чтобы добавить новое поведение, добавляется новый тип, реализующий интерфейс, без модификации существующей логики (Open/Closed принцип).

Формы полиморфизма, релевантные в прикладной разработке:

  • Полиморфизм подтипов (subtype polymorphism):
    • класс/тип реализует интерфейс или наследует базовый тип;
    • вызывающий код работает с базовым типом/интерфейсом.
  • В Go — через интерфейсы; в классических ООП-языках — через наследование базового класса/интерфейса.

Примеры на Go (интерфейсы как инструмент полиморфизма):

  1. Нотификации в системе (email, Slack, SMS) — классический пример:
type Notifier interface {
Notify(message string) error
}

type EmailNotifier struct {
// config, smtp, etc
}

func (n *EmailNotifier) Notify(message string) error {
// отправка email
return nil
}

type SlackNotifier struct {
// webhook URL, etc
}

func (n *SlackNotifier) Notify(message string) error {
// отправка в Slack
return nil
}

func SendSystemAlert(n Notifier, msg string) error {
return n.Notify(msg)
}
  • SendSystemAlert не знает, чем именно является n — EmailNotifier или SlackNotifier.
  • Поведение выбирается в момент подстановки конкретной реализации, без изменения функции.
  1. Хранилище документов (PostgreSQL vs in-memory) — полезно для тестов и микросервисов:
type Document struct {
ID int
Title string
}

type DocumentRepository interface {
Save(doc *Document) error
FindByID(id int) (*Document, error)
}

type PgDocumentRepository struct {
// db *sql.DB
}

func (r *PgDocumentRepository) Save(doc *Document) error {
// INSERT/UPDATE в PostgreSQL
return nil
}

func (r *PgDocumentRepository) FindByID(id int) (*Document, error) {
// SELECT из PostgreSQL
return &Document{ID: 1, Title: "From DB"}, nil
}

type InMemoryDocumentRepository struct {
data map[int]*Document
}

func (r *InMemoryDocumentRepository) Save(doc *Document) error {
if r.data == nil {
r.data = make(map[int]*Document)
}
r.data[doc.ID] = doc
return nil
}

func (r *InMemoryDocumentRepository) FindByID(id int) (*Document, error) {
return r.data[id], nil
}

func GetDocumentTitle(repo DocumentRepository, id int) (string, error) {
doc, err := repo.FindByID(id)
if err != nil {
return "", err
}
if doc == nil {
return "", nil
}
return doc.Title, nil
}
  • Бизнес-логика (GetDocumentTitle) не зависит от того, где хранятся данные.
  • Для продакшена используем PgDocumentRepository, для тестов — InMemoryDocumentRepository.
  • Это и есть практический, полезный полиморфизм.

Контраст с анти-паттернами:

  • Отсутствие полиморфизма:
    • if storageType == "pg" { ... } else if storageType == "memory" { ... } разбросано по коду.
    • Жёсткая связность, сложность расширения.
  • Правильное использование:
    • единый интерфейс;
    • выбор реализации на конфигурационном/композиционном уровне.

Важно в контексте современных сервисов:

  • Полиморфизм:
    • основа для тестируемости (mock/stub реализации интерфейсов),
    • ключевой механизм при построении многослойной архитектуры (transport → service → repository),
    • позволяет легко интегрироваться с разными внешними системами, не переписывая доменную логику.

Кратко: полиморфизм — это не просто "разные классы с одинаковыми методами", а способность системы подставлять разные реализации за единым контрактом, удерживая стабильным интерфейс и изолируя изменения.

Вопрос 11. Что такое интерфейс в PHP и для чего он нужен?

Таймкод: 00:07:03

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

Правильный ответ:

Интерфейс в PHP — это контракт, который задаёт набор публичных методов (и с PHP 8.1 — также констант и, частично, поведения через readonly/enum и т.п.), без реализации логики по умолчанию (за исключением trait, но это другое). Любой класс, реализующий интерфейс, обязан реализовать все его методы с совместимой сигнатурой.

Основные цели интерфейсов:

  • определение контракта между частями системы;
  • обеспечение полиморфизма: возможность работать с разными реализациями через общий тип;
  • снижение связности: код зависит от абстракций, а не от конкретных классов;
  • облегчение тестирования (mock/stub реализации);
  • упрощение замены и расширения реализаций без изменения клиентского кода.

Ключевые свойства интерфейса в PHP:

  • содержит объявления методов без реализации (до PHP 8, далее возможны некоторые усовершенствования, но базовый принцип сохраняется);
  • методы интерфейса всегда публичные;
  • класс может реализовывать несколько интерфейсов;
  • интерфейс может наследовать другой интерфейс.

Простой пример интерфейса в PHP:

<?php

interface LoggerInterface
{
public function info(string $message): void;
public function error(string $message, array $context = []): void;
}

class FileLogger implements LoggerInterface
{
private string $file;

public function __construct(string $file)
{
$this->file = $file;
}

public function info(string $message): void
{
file_put_contents($this->file, "[INFO] {$message}\n", FILE_APPEND);
}

public function error(string $message, array $context = []): void
{
$contextStr = json_encode($context);
file_put_contents($this->file, "[ERROR] {$message} {$contextStr}\n", FILE_APPEND);
}
}

class StdoutLogger implements LoggerInterface
{
public function info(string $message): void
{
echo "[INFO] {$message}\n";
}

public function error(string $message, array $context = []): void
{
echo "[ERROR] {$message} " . json_encode($context) . "\n";
}
}

function processSomething(LoggerInterface $logger): void
{
$logger->info("Start processing");
// ...
$logger->info("Done");
}

Здесь:

  • LoggerInterface задаёт контракт логирования.
  • FileLogger и StdoutLogger реализуют интерфейс по-разному.
  • processSomething работает с LoggerInterface, не зависим от конкретного способа логирования — это классический полиморфизм через интерфейс.

Почему это важно и как переносится на Go:

Хотя вопрос про PHP, для подготовки к Go стоит видеть параллели:

  • В Go интерфейсы также задают поведение (набор методов), а типы "неявно" реализуют интерфейсы, если их методы совпадают по сигнатуре.
  • Это ещё сильнее снижает связность: не нужно явно указывать implements.

Аналогичный пример на Go:

type Logger interface {
Info(msg string)
Error(msg string, fields map[string]any)
}

type StdoutLogger struct{}

func (l *StdoutLogger) Info(msg string) {
println("[INFO]", msg)
}

func (l *StdoutLogger) Error(msg string, fields map[string]any) {
println("[ERROR]", msg, fields)
}

func process(logger Logger) {
logger.Info("start")
// ...
logger.Info("done")
}

И в PHP, и в Go ключевая идея одна:

  • интерфейс определяет "что умеет объект";
  • конкретный тип определяет "как это сделать";
  • потребитель зависит от интерфейса, а не от реализации, что критично для:
    • модульного тестирования (mock/stub),
    • гибкой архитектуры,
    • замены инфраструктурных деталей без переписывания доменной логики.

Вопрос 12. Зачем нужны абстрактные классы, если уже есть интерфейсы?

Таймкод: 00:07:43

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

Правильный ответ:

Интерфейс определяет только контракт: какие методы должны быть, без деталей реализации и без состояния.
Абстрактный класс сочетает в себе:

  • общий контракт (абстрактные методы);
  • общее поведение (уже реализованные методы);
  • общее состояние (свойства, защищённые поля);
  • единое место, где можно зафиксировать инварианты и базовую логику.

Поэтому абстрактный класс нужен, когда:

  1. Несколько классов:

    • разделяют общий набор свойств (state);
    • используют частично одинаковую реализацию;
    • но при этом требуют доопределения/специализации некоторых частей.
  2. Нужно:

    • избежать дублирования кода между реализациями;
    • централизованно задать базовые гарантии, проверки, инфраструктурное поведение;
    • принудить наследников реализовать определённые методы (абстрактные).

Интерфейс отвечает на вопрос:

  • "Что этот тип умеет делать?"

Абстрактный класс отвечает на вопросы:

  • "Что он умеет делать?"
  • "Какая часть этого поведения и состояния уже общая для всех наследников?"
  • "Какие точки расширения должны быть реализованы в потомках?"

Важный практический вывод:

  • Интерфейс — для слабой связности и полиморфизма.
  • Абстрактный класс — для разделяемой реализации и общей модели поведения внутри одной иерархии.

Пример на PHP: общий базовый класс для репозиториев

<?php

interface DocumentRepositoryInterface
{
public function find(int $id): ?array;
public function save(array $data): int;
}

abstract class AbstractPdoRepository implements DocumentRepositoryInterface
{
protected \PDO $pdo;
protected string $table;

public function __construct(\PDO $pdo, string $table)
{
$this->pdo = $pdo;
$this->table = $table;
}

public function find(int $id): ?array
{
$stmt = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE id = :id");
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);

return $row ?: null;
}

// save оставляем абстрактным, т.к. структура данных у разных сущностей различается
abstract public function save(array $data): int;
}

class DocumentRepository extends AbstractPdoRepository
{
public function __construct(\PDO $pdo)
{
parent::__construct($pdo, 'documents');
}

public function save(array $data): int
{
// конкретная логика INSERT/UPDATE для таблицы documents
$stmt = $this->pdo->prepare(
"INSERT INTO documents (title, content) VALUES (:title, :content) RETURNING id"
);
$stmt->execute([
'title' => $data['title'],
'content' => $data['content'],
]);

return (int)$stmt->fetchColumn();
}
}

Что здесь важно:

  • DocumentRepositoryInterface задаёт внешний контракт.
  • AbstractPdoRepository:
    • реализует часть контракта (find);
    • хранит общее состояние ($pdo, $table);
    • заставляет наследников реализовать save.
  • DocumentRepository:
    • не дублирует код работы с PDO и таблицей;
    • концентрируется на специфике сущности.

Такой подход:

  • уменьшает дублирование;
  • делает код более согласованным;
  • но при этом не мешает, при необходимости, иметь другие реализации интерфейса, не наследующиеся от AbstractPdoRepository (например, in-memory или HTTP-клиент к внешнему сервису).

Сопоставление с Go (для понимания архитектурно):

  • В Go нет абстрактных классов и наследования, но тот же эффект достигается:
    • через композицию (встраивание структур);
    • через интерфейсы для контрактов;
    • через общие вспомогательные функции.
  • В языках с наследованием абстрактный класс — это инструмент для организации общей реализации.
  • Интерфейс — базовый инструмент для проектирования границ между компонентами в любом стеке.

Кратко:

  • Интерфейс — "что требуется".
  • Абстрактный класс — "что требуется" + "вот общая часть реализации и состояния".
  • Они не взаимоисключают, а дополняют друг друга: интерфейсы для внешних контрактов, абстрактные базовые классы для реализации внутри конкретной иерархии.

Вопрос 13. Приходилось ли тебе работать с магическими методами в PHP и какие из них ты знаешь?

Таймкод: 00:08:18

Ответ собеседника: неполный. Упоминает конструктор, метод преобразования в строку, __call, неуверенно ссылается на статические вызовы, деструктор, __set; демонстрирует общее понимание, но не даёт системного и точного перечня и назначения.

Правильный ответ:

Магические методы в PHP — это специальные методы, имена которых начинаются с __ и которые интерпретатор вызывает автоматически в ответ на определённые действия с объектами. Они не должны вызываться напрямую в нормальном коде (хотя технически можно), а предназначены для перехвата поведения, расширения возможностей ООП и реализации "синтаксического сахара".

Важно:

  • знать основные магические методы и когда они срабатывают;
  • использовать их осознанно, чтобы не ухудшать читаемость и предсказуемость кода;
  • не злоупотреблять ими ради "магии", особенно в больших проектах.

Ключевые магические методы (основные, которые чаще всего ожидают на собеседовании):

  1. Жизненный цикл объекта
  • __construct(...)
    • Вызывается при создании объекта.
    • Используется для инициализации состояния, внедрения зависимостей.
  • __destruct()
    • Вызывается при уничтожении объекта (завершении скрипта или потере всех ссылок).
    • Используется для освобождения ресурсов (закрытие файлов, соединений), но в реальных проектах полагаться на него нужно аккуратно.
  1. Доступ к свойствам
  • __get($name)
    • Вызывается при чтении недоступного (приватного/защищённого/несуществующего) свойства.
    • Применения:
      • ленивые вычисления;
      • проксирование доступа к вложенным объектам;
      • совместимость со старым кодом.
  • __set($name, $value)
    • Вызывается при записи в недоступное свойство.
    • Применения:
      • валидация;
      • динамические свойства;
      • маппинг на внутренние структуры.
  • __isset($name)
    • Вызывается при isset() или empty() на недоступном свойстве.
  • __unset($name)
    • Вызывается при unset() недоступного свойства.

Пример:

class Config
{
private array $data = [];

public function __get(string $name)
{
return $this->data[$name] ?? null;
}

public function __set(string $name, $value)
{
$this->data[$name] = $value;
}
}

$config = new Config();
$config->db_host = 'localhost';
echo $config->db_host; // localhost (через __get/__set)
  1. Вызов методов
  • __call($name, $arguments)
    • Вызывается при обращении к недоступному (несуществующему/приватному/protected) методу экземпляра.
  • __callStatic($name, $arguments)
    • Аналогично, но для статических методов.

Использование:

  • прокси-объекты;
  • динамическая маршрутизация методов;
  • адаптеры над внешними библиотеками.

Важно: чрезмерное использование __call/__callStatic усложняет навигацию по коду и статический анализ.

Пример:

class ServiceProxy
{
public function __call($name, $arguments)
{
// логирование, метрики, проброс в реальный сервис
// ...
}
}
  1. Представление объекта
  • __toString()
    • Определяет, как объект преобразуется в строку (например, при echo).
    • Должен возвращать строку; бросать исключения нельзя до PHP 8 (иначе fatal), в новых версиях аккуратнее, но лучше не злоупотреблять.

Пример:

class Document
{
public function __construct(
private int $id,
private string $title
) {}

public function __toString(): string
{
return "Document #{$this->id}: {$this->title}";
}
}
  1. Клонирование
  • __clone()
    • Вызывается при clone $obj.
    • Используется для:
      • глубокого копирования (клонирование вложенных объектов),
      • сброса идентификаторов, состояний, которые не должны копироваться напрямую.

Пример:

class Entity
{
public int $id;
public function __clone()
{
$this->id = 0; // новый объект не должен наследовать ID
}
}
  1. Сериализация / десериализация (до PHP 8.1 и после)

Исторически:

  • __sleep() — вызывается перед serialize(), возвращает список свойств для сериализации.
  • __wakeup() — вызывается при unserialize(), для восстановления ресурсов.

С PHP 7.4+ / 8+:

  • предпочтительнее использовать:
    • __serialize(): array
    • __unserialize(array $data): void
  • Это даёт более явный и безопасный контроль.

Пример:

class SessionData
{
private $connection;
private array $state;

public function __serialize(): array
{
return ['state' => $this->state];
}

public function __unserialize(array $data): void
{
$this->state = $data['state'];
// восстановить соединение при необходимости
}
}
  1. Другие полезные магические методы
  • __invoke()
    • Вызывается, когда объект используется как функция: $obj().
    • Удобен для callback-объектов, middleware, стратегий.
  • __debugInfo()
    • Определяет, какие данные показывать при var_dump($obj).
    • Полезно для скрытия лишнего или чувствительного.

Пример __invoke:

class Validator
{
public function __invoke(string $value): bool
{
return $value !== '';
}
}

$v = new Validator();
var_dump($v("test")); // true

Выводы и рекомендации:

  • Магические методы — мощный инструмент для:
    • удобных API;
    • прокси/обёрток;
    • ленивой инициализации;
    • сериализации, логирования, представления объектов.
  • Но:
    • не стоит превращать код в "чёрную магию";
    • лучше явно объявлять методы и свойства, когда это возможно;
    • магию оставлять для инфраструктурного слоя (ORM, DI-контейнеры, прокси), а не доменной логики.

Для кандидата важно:

  • уверенно перечислить ключевые методы: __construct, __destruct, __get, __set, __isset, __unset, __call, __callStatic, __toString, __clone, __sleep / __wakeup, __serialize / __unserialize, __invoke, __debugInfo;
  • коротко пояснить, когда и зачем используются;
  • подчеркнуть, что использовать их нужно осознанно, особенно в больших и долгоживущих системах.

Вопрос 14. Что такое деструктор в PHP и когда он вызывается?

Таймкод: 00:08:48

Ответ собеседника: правильный. Описывает деструктор как метод, автоматически вызываемый при уничтожении объекта, когда на него больше нет ссылок.

Правильный ответ:

Деструктор в PHP — это магический метод __destruct(), который вызывается интерпретатором при "завершении жизненного цикла" объекта.

Ключевые моменты:

  • Сигнатура:

    class Example {
    public function __destruct() {
    // cleanup
    }
    }
  • Когда вызывается на практике:

    • в конце скрипта для всех ещё существующих объектов;
    • когда объект удаляется сборщиком мусора (например, при обрыве всех ссылок, в том числе в циклических структурах, которые GC умеет разорвать);
    • при принудительном unset($obj) — не всегда немедленно, но может триггерить уничтожение, если не осталось других ссылок.
  • Типичные сценарии использования:

    • освобождение внешних ресурсов:
      • закрытие файловых дескрипторов;
      • закрытие нестандартных подключений (сокеты, внешние хендлы), если они не завершаются автоматически;
    • финализация логирования или временных структур;
    • отладочные сообщения (в dev-среде).
  • Важные нюансы:

    • в веб-приложениях на PHP процесс обычно "одноразовый": после ответа на запрос всё завершается, а ресурсы освобождаются процессом/интерпретатором, поэтому деструкторы используются реже и не так критичны, как, например, в long-running процессах;
    • не стоит полагаться на деструктор для "важной бизнес-логики" (сохранение данных, критичные операции) — момент вызова не гарантирован жёстко в сложных сценариях;
    • деструкторы могут усложнить сборку мусора при циклических ссылках, если реализованы неаккуратно.

Простой пример:

class FileWriter
{
private $handle;

public function __construct(string $filename)
{
$this->handle = fopen($filename, 'a');
}

public function write(string $text): void
{
fwrite($this->handle, $text . PHP_EOL);
}

public function __destruct()
{
if (is_resource($this->handle)) {
fclose($this->handle);
}
}
}

$w = new FileWriter('/tmp/log.txt');
$w->write("Hello");
// При завершении скрипта (или когда объект будет собран GC)
// автоматически вызовется __destruct(), закроется файл.

Кратко: деструктор — это механизм финализации объекта, автоматически вызываемый при его уничтожении; использовать его нужно для аккуратного освобождения ресурсов, но не как основу критичных бизнес-операций.

Вопрос 15. Приходилось ли тебе самостоятельно разрабатывать API?

Таймкод: 00:09:16

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

Правильный ответ:

На такой вопрос важно показать не только факт "делал/не делал один", а понимание полного цикла разработки API и ключевых инженерных решений. Ответ должен демонстрировать зрелое представление о том, как правильно спроектировать и реализовать API, даже если в прошлом это делалось в команде.

Хороший развернутый ответ может выглядеть так:

В рамках разработки API я исходил бы (и исходил на практике в командной работе) из нескольких ключевых этапов:

  1. Анализ домена и требований
  • Понять предметную область:
    • какие сущности есть (например, для документооборота: Document, User, Workflow, Comment, Attachment);
    • какие сценарии должны поддерживаться (создание документа, изменение статуса, согласование, поиск, аудит).
  • Важно начинать не с URL-ов, а с доменной модели и бизнес-процессов.
  1. Проектирование ресурсно-ориентированного API
  • Определение ресурсов и их идентификаторов:
    • GET /documents
    • GET /documents/{id}
    • POST /documents
    • PATCH /documents/{id}
    • POST /documents/{id}/approve
  • Использование корректных HTTP-методов и кодов ответов:
    • 200/201/204 для успешных операций;
    • 400/401/403/404/409/422 для ошибок валидации, прав и конфликтов;
    • 500/502/503 для серверных проблем.
  • Семантика:
    • идемпотентные операции (PUT/DELETE) реализуются так, чтобы быть безопасно повторяемыми;
    • POST используется для создания и неидемпотентных команд.
  1. Формат данных и контракты
  • Обычно JSON как основной формат:
    • чётко задокументированные схемы запросов/ответов;
    • стабильные контракты, без "скрытых" полей.
  • Использование:
    • Content-Type: application/json
    • Accept: application/json
  • При необходимости — версионирование API:
    • через URL (/api/v1/...) или заголовки.

Пример JSON-контракта для создания документа:

POST /documents
{
"title": "Договор №123",
"client_id": 42,
"content": "..."
}

Ответ:

HTTP/1.1 201 Created
Location: /documents/101
{
"id": 101,
"title": "Договор №123",
"client_id": 42,
"status": "draft"
}
  1. Аутентификация и авторизация
  • Использование токенов (JWT, OAuth2, session/cookie для внутренних систем).
  • Явная модель ролей и прав:
    • кто может создавать/просматривать/утверждать документы.
  • Проверка прав на уровне middleware/handler-слоя, а не размазано по коду.
  1. Обработка ошибок и единый формат ответов
  • Единый формат ошибок:
    • код, сообщение, возможно список полей с ошибками.

Пример:

HTTP/1.1 422 Unprocessable Entity
{
"error": "validation_error",
"details": {
"title": "required",
"client_id": "must be positive"
}
}
  • Осмысленные коды HTTP вместо "всегда 200 + текст ошибки".
  1. Наблюдаемость и эксплуатация
  • Логирование:
    • trace-id / request-id;
    • входные запросы, ключевые события, ошибки.
  • Метрики:
    • латентность, RPS, коды ответов.
  • Возможность троттлинга, rate limiting, кэширования (через заголовки).
  1. Документация
  • Использование OpenAPI/Swagger:
    • для генерации документации;
    • для генерации клиентов;
    • как "единого источника правды" по контракту API.
  • Примеры запросов/ответов, описание кодов ошибок.
  1. Реализация на Go (как пример зрелого подхода)

Даже если ранее API делались на PHP, важно уметь перенести те же принципы на Go.

Пример минимального, но корректно спроектированного REST-обработчика:

type Document struct {
ID int64 `json:"id"`
Title string `json:"title"`
ClientID int64 `json:"client_id"`
Status string `json:"status"`
}

type CreateDocumentRequest struct {
Title string `json:"title"`
ClientID int64 `json:"client_id"`
Content string `json:"content"`
}

func (s *Server) createDocument(w http.ResponseWriter, r *http.Request) {
var req CreateDocumentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}

if req.Title == "" || req.ClientID <= 0 {
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(map[string]any{
"error": "validation_error",
})
return
}

// Пример вставки в PostgreSQL (упрощённо)
var id int64
err := s.db.QueryRow(
`INSERT INTO documents (title, client_id, status, content)
VALUES ($1, $2, 'draft', $3) RETURNING id`,
req.Title, req.ClientID, req.Content,
).Scan(&id)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}

doc := Document{
ID: id,
Title: req.Title,
ClientID: req.ClientID,
Status: "draft",
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(doc)
}

Пример SQL-схемы для поддержки такого API:

CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
client_id BIGINT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft',
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_documents_client_id ON documents (client_id);
CREATE INDEX idx_documents_status ON documents (status);

Такой ответ демонстрирует:

  • понимание принципов REST и работы HTTP, которые ты уже описывал ранее;
  • умение спроектировать ресурсы, контракты, статусы, работу с БД;
  • внимание к безопасности, валидации, ошибкам и поддерживаемости.

Даже если в прошлом работа шла под руководством тимлида и по готовым спецификациям, важно показать, что сейчас ты способен осознанно спроектировать и реализовать API end-to-end.

Вопрос 16. Какие проблемы могут возникнуть, если 10 000 пользователей одновременно поставят лайк одному и тому же посту на сайте?

Таймкод: 00:09:38

Ответ собеседника: неполный. Понимает, что при одновременных запросах к одной записи в БД часть лайков может "потеряться" и возможны блокировки, но объясняет без точной терминологии и без разбора конкретных механизмов конкурентного доступа и масштабирования.

Правильный ответ:

Сценарий массовых лайков — классический пример проблем высокой конкурентности, гонок данных и узких мест в архитектуре. Если 10 000 пользователей одновременно лайкнут один и тот же пост, ключевые потенциальные проблемы:

  1. Потеря обновлений (race condition)

Типичная наивная реализация:

  1. Прочитали текущее количество лайков:
SELECT likes FROM posts WHERE id = :post_id;
  1. В приложении увеличили на 1.

  2. Записали:

UPDATE posts SET likes = :likes WHERE id = :post_id;

При высоком параллелизме:

  • несколько запросов читают одно и то же значение (например, 1000);
  • каждый считает likes+1 → 1001;
  • каждый пишет 1001;
  • в итоге после 10 000 лайков число будет далеко не 10 000 + исходное; часть инкрементов потеряется.

Это классическая "потеря обновлений" (lost update).
Решение: атомарные операции на уровне БД или блокировки.

  1. Блокировки и деградация производительности

Если реализовать обновление "правильно, но грубо":

UPDATE posts
SET likes = likes + 1
WHERE id = :post_id;

Это атомарно, но:

  • все 10 000 транзакций пытаются модифицировать одну и ту же строку;
  • возникает очередь ожидания блокировки (row lock);
  • растут задержки, возможны блокировки и эскалация конфликтов;
  • при неаккуратной работе с транзакциями возможны deadlock-и (особенно если внутри одной транзакции обновляется несколько сущностей в разном порядке).

Это не приводит к потере лайков (если всё грамотно), но создаёт узкое место по latency и throughput.

  1. "Горячий ключ" (hotspot) и нагрузка на БД

Единый "счётчик лайков" в одной строке:

  • превращается в горячую точку:
    • высокое количество операций записи по одному ключу;
    • нагрузка на WAL/redo log;
    • ухудшение масштабируемости;
  • БД хорошо масштабирует параллельные операции по разным ключам, но плохо — тысячи конкурентных апдейтов одной строки.

Итог:

  • растёт время ответа API;
  • пользователи видят задержки или "подлагивающий" интерфейс;
  • при пиках это может упираться в пределы одной ноды БД.
  1. Проблемы целостности и повторных лайков

Дополнительные аспекты:

  • двойные лайки:
    • если нет уникального ограничения (post_id, user_id), один пользователь может "залить" много лайков;
  • конкурентное добавление записей:
    • при вставке в таблицу post_likes без уникального индекса для (post_id, user_id) могут появиться дубликаты при гонках;
    • это раздувает данные и ломает статистику.

Решение:

CREATE UNIQUE INDEX ux_post_likes_post_user
ON post_likes(post_id, user_id);

и использовать INSERT ... ON CONFLICT DO NOTHING.

  1. Потенциальные ошибки при неверном выборе уровня изоляции
  • При более слабых уровнях изоляции (READ COMMITTED, REPEATABLE READ, зависит от СУБД) можно столкнуться с:
    • non-repeatable reads;
    • phantom reads;
  • Если логика лайков завязана на промежуточные выборки и агрегации, можно получить неконсистентные данные.

При нормальном UPDATE ... SET likes = likes + 1 это не проблема, но при "чтение + вычисление + запись" — уже да.

Практические решения

Цель — сохранить корректность, убрать потери и масштабировать.

  1. Атомарные апдейты в БД

На уровне SQL:

UPDATE posts
SET likes = likes + 1
WHERE id = $1;
  • операция атомарна;
  • не будет потери лайков;
  • но остаётся проблема hot-row (очереди блокировок и задержки).
  1. Хранить лайки как события (post_likes) + агрегация

Модель:

  • таблица лайков:
CREATE TABLE post_likes (
post_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (post_id, user_id)
);
  • пользовательский лайк:
INSERT INTO post_likes (post_id, user_id)
VALUES ($1, $2)
ON CONFLICT (post_id, user_id) DO NOTHING;
  • счётчик:
    • периодически считаем COUNT(*) по post_likes и кешируем в posts.likes_cache;
    • либо поддерживаем через триггер / background job / материализованные представления.

Преимущества:

  • корректность лайков (нет дублей);
  • параллелизм: блокировки распределяются по множеству строк;
  • можно масштабировать чтение и агрегацию.

Недостатки:

  • дороже по данным;
  • нужно продумывать стратегию агрегации.
  1. Кэш + асинхронная запись (Redis, инкременты)

Схема:

  • входящий запрос:
    • проверяет в БД/кэше, лайкнул ли уже пользователь;
    • если нет — в Redis:
      • INCR для likes:post_id (атомарно);
      • SADD/SETNX для отметки user_id;
  • периодически фоновой задачей:
    • синхронизируем значения из Redis в БД.

Пример (в общих чертах):

// Псевдокод обработчика лайка

func (s *Server) likePost(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
postID := getPostID(r)

// Проверка, что не лайкали ранее (Redis set / bitmap)
already, err := s.redis.SIsMember(ctx, fmt.Sprintf("post:%d:likes_users", postID), userID).Result()
if err != nil {
// fallback: можно проверить в БД, логировать
}
if already {
w.WriteHeader(http.StatusNoContent)
return
}

// Атомарно помечаем лайк
pipe := s.redis.TxPipeline()
pipe.SAdd(ctx, fmt.Sprintf("post:%d:likes_users", postID), userID)
pipe.Incr(ctx, fmt.Sprintf("post:%d:likes_count", postID))
_, err = pipe.Exec(ctx)
if err != nil {
http.Error(w, "try again later", http.StatusServiceUnavailable)
return
}

w.WriteHeader(http.StatusNoContent)
}

Фоновый воркер (псевдологика):

  • периодически:
    • читает likes_count и/или дельту;
    • обновляет posts.likes в БД.
  1. Row-level locking / SELECT FOR UPDATE (если нужен строгий контроль)

Если по каким-то причинам нужно делать read-modify-write:

BEGIN;

SELECT likes
FROM posts
WHERE id = $1
FOR UPDATE;

UPDATE posts
SET likes = likes + 1
WHERE id = $1;

COMMIT;
  • FOR UPDATE ставит блокировку на строку до конца транзакции;
  • предотвращает потерю обновлений;
  • но полностью сериализует операции по одной строке: при 10 000 запросов latency возрастёт.

Это вариант для низких объёмов или крайне критичных счётчиков; для лайков на массовых сервисах обычно выбирают другие подходы.

Краткое резюме, которое ожидают услышать:

  • Проблемы:
    • потеря лайков из-за гонок (lost updates);
    • блокировки строки и рост latency;
    • узкое место на уровне одной записи (hotspot);
    • риск дублей лайков от одного пользователя;
    • повышенная нагрузка на БД при пиковых событиях.
  • Инженерные решения:
    • атомарные инкременты на уровне БД;
    • уникальные индексы (post_id, user_id) для защиты от дублей;
    • вынос лайков в отдельную таблицу + агрегирование;
    • использование Redis/кэша + асинхронная синхронизация;
    • аккуратное использование транзакций и SELECT FOR UPDATE там, где требуется строгая консистентность.

Такой ответ показывает понимание конкурентного доступа, транзакций, масштабирования и практической архитектуры высоконагруженных систем.

Вопрос 17. Какие решения на уровне базы данных можно применить, чтобы при массовых лайках гарантировать корректную запись значений?

Таймкод: 00:11:31

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

Правильный ответ:

Для массовых лайков ключевая цель на уровне БД — обеспечить:

  • отсутствие потери инкрементов (консистентность);
  • защиту от двойных лайков пользователя;
  • предсказуемое поведение под высокой конкуренцией;
  • по возможности — минимизацию блокировок и "горячих" точек.

Разберём основные подходы и конкретные техники.

  1. Атомарное обновление счётчика в одной строке

Наивный (неправильный) подход:

-- Плохо: read + write
SELECT likes FROM posts WHERE id = $1;
UPDATE posts SET likes = $likes + 1 WHERE id = $1;

Проблема: lost updates при конкурентном доступе.

Правильный атомарный вариант:

UPDATE posts
SET likes = likes + 1
WHERE id = $1;

Особенности:

  • Операция атомарна: каждая транзакция увеличит значение независимо от других.
  • Нет потери лайков.
  • Минус: все обращения бьются в одну строку → row-level lock очередь → "hot row".

Применимо:

  • при умеренной нагрузке;
  • как базовый надёжный вариант.
  1. Использование таблицы лайков с уникальным ограничением

Вместо прямого счётчика:

  • Храним каждый лайк как запись (post_id, user_id):
CREATE TABLE post_likes (
post_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (post_id, user_id)
);
  • При лайке:
INSERT INTO post_likes (post_id, user_id)
VALUES ($1, $2)
ON CONFLICT (post_id, user_id) DO NOTHING;

Гарантии и преимущества:

  • Уникальный индекс гарантирует: один пользователь — один лайк к посту.
  • Корректность при гонках:
    • два конкурентных запроса от одного user_id:
      • один пройдет,
      • второй упрётся в ON CONFLICT DO NOTHING.
  • Потерь лайков от разных пользователей нет: каждая пара (post_id, user_id) независима.

Как считать лайки:

  • В лоб:

    SELECT count(*) FROM post_likes WHERE post_id = $1;

    Но при больших объёмах — тяжёлое место.

  • Оптимизация:

    • денормализованный счётчик в posts.likes_count;
    • обновление через триггеры или фоновые задачи.

Вариант с триггером (PostgreSQL):

ALTER TABLE posts ADD COLUMN likes_count BIGINT NOT NULL DEFAULT 0;

CREATE OR REPLACE FUNCTION inc_likes_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE posts SET likes_count = likes_count + 1 WHERE id = NEW.post_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE posts SET likes_count = likes_count - 1 WHERE id = OLD.post_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_post_likes_count
AFTER INSERT OR DELETE ON post_likes
FOR EACH ROW EXECUTE FUNCTION inc_likes_count();

Это:

  • убирает потерю лайков;
  • переносит конкуренцию на множество строк post_likes, а не одну posts;
  • даёт устойчивую модель для высоких нагрузок.
  1. SELECT ... FOR UPDATE для строгой последовательности

Если всё-таки используется схема "прочитал — посчитал — записал", то для избежания lost updates:

BEGIN;

SELECT likes
FROM posts
WHERE id = $1
FOR UPDATE;

UPDATE posts
SET likes = likes + 1
WHERE id = $1;

COMMIT;

Зачем FOR UPDATE:

  • ставит эксклюзивную блокировку на строку до конца транзакции;
  • не позволяет другим транзакциям читать её для модификации без ожидания.

Плюсы:

  • корректность инкрементов.

Минусы:

  • жесткая сериализация по одной строке;
  • при 10 000 одновременных запросов: огромная очередь → большая задержка → потенциальные таймауты.

Применять:

  • для более "толстых" бизнес-операций, где важна строгая последовательность;
  • но для простого счётчика лайков на высоких нагрузках лучше искать более масштабируемые схемы.
  1. Изоляция транзакций и защита от аномалий

На уровне БД важно:

  • использовать корректный уровень изоляции:
    • READ COMMITTED (по умолчанию в PostgreSQL) обычно достаточно при атомарных UPDATE ... SET likes = likes + 1;
    • при сложных вычислениях — возможно SERIALIZABLE, но это дороже.
  • избегать "read-modify-write" без блокировок;
  • не пытаться реализовывать счётчик через аггрегаты без учёта конкуренции.
  1. Асинхронная агрегация + журнал событий (event sourcing-подход)

На уровне БД:

  • лайки — отдельные записи (как в п.2);
  • счётчики в posts.likes_count обновляются:
    • периодическими batch-джобами:
      UPDATE posts p
      SET likes_count = c.cnt
      FROM (
      SELECT post_id, count(*) AS cnt
      FROM post_likes
      GROUP BY post_id
      ) c
      WHERE p.id = c.post_id;
    • или инкрементально (обработка логов/очередей).

Таким образом:

  • запись лайков — полностью корректна;
  • горячий путь (insert) простой и быстрый;
  • счётчики могут слегка отставать (eventual consistency), но без потерь.
  1. Использование RETURNING / UPSERT для атомарности

При работе с лайками/счётчиками желательно использовать один SQL-стейтмент:

  • для вставки лайка и инкремента:
    • через триггер ИЛИ
    • через отдельную команду с ON CONFLICT.

Для PostgreSQL:

INSERT INTO post_likes (post_id, user_id)
VALUES ($1, $2)
ON CONFLICT (post_id, user_id) DO NOTHING;

Далее в приложении:

  • если affected rows = 1 → инкрементировать счётчик;
  • если 0 → значит лайк уже был, ничего не делаем.

Инкремент:

UPDATE posts
SET likes_count = likes_count + 1
WHERE id = $1;

При необходимости — обернуть оба шага в транзакцию, чтобы избежать рассинхронизации.

  1. Что важно уметь проговорить на собеседовании

Краткий, зрелый ответ может выглядеть так:

  • Не читать и не инкрементировать лайки в приложении — только атомарные операции на уровне БД.
  • Использовать:
    • UPDATE posts SET likes = likes + 1 WHERE id = :id для начала;
    • уникальный индекс (post_id, user_id) и INSERT ... ON CONFLICT DO NOTHING для защиты от двойных лайков;
    • отдельную таблицу post_likes и денормализованный счётчик для масштабируемости.
  • Понимать:
    • row-level locking и "горячие" строки;
    • SELECT ... FOR UPDATE как инструмент строгой последовательности, но не панацею под 10 000 RPS;
    • необходимость в некоторых случаях переходить к асинхронной агрегации и кешам.

Таким образом, решения на уровне БД — это комбинация:

  • атомарных SQL-операций,
  • корректных уникальных ограничений,
  • продуманного моделирования (отдельная таблица лайков),
  • при необходимости — триггеров/_BATCH-обновлений для агрегатов,

что гарантирует корректность значений даже при очень высокой конкурентной нагрузке.

Вопрос 18. Что такое транзакция в базе данных и как она работает?

Таймкод: 00:13:02

Ответ собеседника: правильный. Определяет транзакцию как набор операций, которые либо выполняются целиком, либо при ошибке откатываются; упоминает BEGIN TRANSACTION и COMMIT.

Правильный ответ:

Транзакция в базе данных — это логическая единица работы, объединяющая несколько операций (INSERT/UPDATE/DELETE/SELECT и др.) таким образом, что система гарантирует согласованность данных: либо все операции внутри транзакции фиксируются, либо ни одна (в случае ошибки или отката).

Базовая модель транзакций формализуется через свойства ACID. Понимание их — ключ к корректному поведению в реальных системах.

ACID-свойства транзакций

  1. Atomicity (атомарность)
  • "Все или ничего."
  • Если часть операций не может быть выполнена (ошибка, нарушение ограничения, таймаут), вся транзакция откатывается.
  • Не бывает "наполовину записали деньги, наполовину нет".
  • Реализуется через механизмы журнала (WAL), откатных записей и служебных структур в СУБД.
  1. Consistency (согласованность)
  • Транзакция переводит базу данных из одного согласованного состояния в другое.
  • Все ограничения (PRIMARY KEY, UNIQUE, FOREIGN KEY, CHECK, бизнес-правила) должны выполняться до и после транзакции.
  • Если операция нарушает ограничения — транзакция откатывается.

Важно: согласованность — ответственность и БД (через ограничения), и приложения (через корректную бизнес-логику).

  1. Isolation (изолированность)
  • Параллельные транзакции не должны ломать друг другу логику.
  • Каждая транзакция должна "видеть" данные так, как будто она выполняется одна (в идеале).
  • Изоляция реализуется через блокировки и/или MVCC (многоверсионность).
  • На практике используются уровни изоляции (ниже).
  1. Durability (долговечность)
  • После COMMIT данные не должны потеряться при сбоях.
  • СУБД гарантирует запись изменений в надёжное хранилище (через WAL, fsync, репликацию).
  • При рестарте БД восстановит состояние, учитывая закоммиченные транзакции и откатывая незавершённые.

Как транзакция работает технически (упрощённо)

Типичный цикл (PostgreSQL / большинство SQL-СУБД):

  1. Старт транзакции:
BEGIN;
-- или START TRANSACTION;
  1. Выполнение нескольких запросов:
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

3a) Фиксация:

COMMIT;
  • Все изменения становятся видимыми другим транзакциям.
  • БД гарантирует сохранность.

3b) Откат:

ROLLBACK;
  • Все изменения внутри транзакции отменяются.
  • Состояние БД возвращается к точке перед BEGIN.

Пример: перевод денег между счетами

Без транзакции (плохо):

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- сбой здесь
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

Может списать деньги с одного счета и не зачислить на другой.

С транзакцией (правильно):

BEGIN;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

COMMIT;

Если любая из операций падает:

  • вызываем ROLLBACK, обе операции откатываются;
  • инвариант "суммарный баланс" сохраняется.

Уровни изоляции транзакций (важно упомянуть)

Разные уровни позволяют балансировать между корректностью и производительностью. Классические уровни (SQL стандарт):

  • READ UNCOMMITTED
    • Практически не используется; допускает "грязные чтения".
  • READ COMMITTED (часто по умолчанию, например в PostgreSQL)
    • Транзакция видит только закоммиченные изменения других транзакций.
    • Возможны "non-repeatable read" и "phantom read".
  • REPEATABLE READ
    • Повторное чтение одних и тех же строк вернёт те же значения.
    • В PostgreSQL реализован через MVCC как "snapshot isolation"; хорошо защищает от многого.
  • SERIALIZABLE
    • Максимальная изоляция: система ведёт себя так, как будто транзакции выполняются последовательно.
    • Самый дорогой по ресурсам; возможны serialization failures, которые нужно уметь ретраить.

Практические эффекты:

  • Более высокая изоляция:
    • меньше аномалий;
    • больше блокировок/конфликтов.
  • В реальных сервисах:
    • READ COMMITTED или REPEATABLE READ + аккуратный дизайн запросов и блокировок;
    • SERIALIZABLE точечно там, где критична строгая консистентность.

Пример на Go с транзакцией (database/sql + PostgreSQL)

func TransferMoney(ctx context.Context, db *sql.DB, fromID, toID int64, amount int64) error {
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable, // или LevelReadCommitted/RepeatableRead
})
if err != nil {
return err
}

defer func() {
if p := recover(); p != nil {
_ = tx.Rollback()
panic(p)
}
}()

// Списание
if _, err := tx.ExecContext(ctx,
`UPDATE accounts SET balance = balance - $1 WHERE id = $2`,
amount, fromID,
); err != nil {
_ = tx.Rollback()
return err
}

// Зачисление
if _, err := tx.ExecContext(ctx,
`UPDATE accounts SET balance = balance + $1 WHERE id = $2`,
amount, toID,
); err != nil {
_ = tx.Rollback()
return err
}

if err := tx.Commit(); err != nil {
return err // при SERIALIZABLE могут быть конфликты → нужно уметь ретраить
}

return nil
}

Ключевые моменты в таком коде:

  • все связанные операции выполняются в одной транзакции;
  • при любой ошибке — откат;
  • при успешном завершении — COMMIT и гарантированная фиксация.

Основные тезисы, которые важно уверенно донести на собеседовании:

  • транзакция — это механизм обеспечения целостности и согласованности при выполнении нескольких связанных операций;
  • "всё или ничего", откат при ошибках;
  • ACID — фундамент: атомарность, согласованность, изоляция, долговечность;
  • осознанный выбор уровня изоляции и работа с конкуренцией (блокировки, MVCC, возможные конфликты);
  • умение корректно использовать транзакции в коде приложения (в т.ч. на Go) при работе с PostgreSQL/MySQL и т.п.

Вопрос 19. Что такое очередь как структура данных и приходилось ли тебе её применять на практике?

Таймкод: 00:13:36

Ответ собеседника: неправильный. Определяет очередь как структуру по принципу "первым пришёл, последним ушёл" (это LIFO, стек, а не очередь) и отмечает отсутствие практического опыта работы с очередями.

Правильный ответ:

Очередь (queue) — это базовая структура данных с семантикой FIFO (First In, First Out):

  • элемент, который первым добавили, первым и будет извлечён;
  • аналог живой очереди в магазине: кто раньше встал, того раньше обслужили.

Базовые операции очереди:

  • enqueue (push) — добавить элемент в конец очереди;
  • dequeue (pop) — удалить и вернуть элемент из начала очереди;
  • front/peek — посмотреть элемент в начале, не удаляя;
  • isEmpty/len — проверка пустоты/размера.

Отличие от стека:

  • стек — LIFO (Last In, First Out): "последним пришёл — первым ушёл";
  • очередь — FIFO: "первым пришёл — первым ушёл".

Практическое применение очередей

Очереди широко используются и в алгоритмах, и в архитектуре приложений:

  • Внутри одного процесса:
    • обход в ширину (BFS) в графах;
    • планирование задач;
    • буферизация данных.
  • В распределённых системах и backend-разработке:
    • message queue (RabbitMQ, Kafka, NATS, SQS, Google Pub/Sub);
    • обработка фоновых задач (email-рассылка, генерация отчётов, конвертация файлов);
    • разгрузка "горячих" операций от онлайн-запросов;
    • ретраи и устойчивость к временным сбоям внешних сервисов.

Ключевая идея на уровне архитектуры:

  • HTTP-запрос обрабатывает только постановку задачи в очередь (быстро);
  • тяжёлая логика обрабатывается асинхронно воркерами, читающими сообщения из очереди;
  • это повышает отзывчивость системы и устойчивость под нагрузкой.

Пример простой очереди на Go (структура данных в памяти)

type Queue[T any] struct {
data []T
head int
}

func NewQueue[T any]() *Queue[T] {
return &Queue[T]{data: make([]T, 0)}
}

func (q *Queue[T]) Enqueue(v T) {
q.data = append(q.data, v)
}

func (q *Queue[T]) Dequeue() (T, bool) {
var zero T
if q.IsEmpty() {
return zero, false
}
v := q.data[q.head]
q.head++
// Опционально: периодически "подчищать" срез, если head сильно ушёл
return v, true
}

func (q *Queue[T]) IsEmpty() bool {
return q.head >= len(q.data)
}

Пример использования очереди задач в реальном сервисе

Сценарий: при лайке/регистрации/создании документа отправлять email не синхронно, а через очередь.

  1. В HTTP-хендлере:
func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
// ... создаём пользователя в БД

// Пишем задачу в очередь (псевдокод)
task := EmailTask{
To: user.Email,
Subject: "Welcome",
Body: "Спасибо за регистрацию",
}
if err := s.queue.Publish(task); err != nil {
// логируем, можно вернуть 202 Accepted или 500, в зависимости от критичности
}

w.WriteHeader(http.StatusCreated)
}
  1. Фоновый воркер:
func (w *Worker) Run() {
for {
task, ok := w.queue.Consume()
if !ok {
time.Sleep(time.Millisecond * 100)
continue
}
// отправка email, ретраи, логирование
}
}

(В реальности queue — это RabbitMQ/Kafka/SQS, а не in-memory структура.)

Очередь как внешний инфраструктурный компонент

Для продакшена чаще используют готовые системы:

  • RabbitMQ:
    • классическая брокер-очередь (AMQP), подтверждения, маршрутизация, retry/dlx.
  • Kafka:
    • распределённый лог/очередь для больших потоков событий.
  • Redis Streams / Lists:
    • простая очередь/стрим для задач.
  • SQS, Pub/Sub:
    • managed-очереди в облаках.

Что важно уметь объяснить:

  • зачем:
    • разделить приём запроса и тяжёлую обработку;
    • сгладить пики нагрузки;
    • повысить отказоустойчивость;
  • как:
    • producer пишет сообщения (задачи) в очередь;
    • consumer(ы) читают и обрабатывают;
    • сообщения удаляются/подтверждаются только после успешной обработки.

Кратко для собеседования:

  • Очередь — это FIFO-структура: первым вошёл — первым вышел.
  • Отличается от стека (LIFO).
  • Используется:
    • как базовый алгоритмический инструмент (BFS, буферы);
    • как фундаментальный паттерн в системной архитектуре: асинхронная обработка, message-driven взаимодействие, разгрузка API.
  • Важно понимать не только определение, но и практику применения в бекенде и микросервисах.

Вопрос 20. Что такое нормализация базы данных и какие нормальные формы ты знаешь?

Таймкод: 00:14:19

Ответ собеседника: неправильный. Утверждает, что есть 6 форм нормализации, но даёт некорректные определения: для 1НФ говорит про уникальность значений, для 2НФ и 3НФ — путаные и частично неверные формулировки зависимостей. Общая идея разбиения данных по таблицам упомянута, но без точного понимания нормальных форм.

Правильный ответ:

Нормализация базы данных — это процесс проектирования структуры таблиц таким образом, чтобы:

  • минимизировать дублирование данных;
  • устранить аномалии при вставке, обновлении и удалении;
  • обеспечить логическую целостность и предсказуемость поведения данных.

Основной инструмент нормализации — анализ функциональных зависимостей между атрибутами (полями) и декомпозиция таблиц на основе этих зависимостей.

На практике в прикладных системах (PostgreSQL/MySQL) обычно достаточно уверенно владеть 1НФ, 2НФ, 3НФ и понимать BCNF. Более высокие нормальные формы встречаются редко и нужны в специфичных задачах.

Ниже — кратко и по делу, с акцентом на правильные определения и примеры.

Нулевая интуиция: зачем вообще нормализация?

Если спроектировать таблицу "на глаз", без нормализации, часто получаем:

  • дублирование: одни и те же данные (имя клиента, адрес, название отдела) повторяются во множестве строк;
  • аномалии обновления:
    • изменили название отдела в одной строке, забыли в других;
  • аномалии вставки:
    • нельзя добавить отдел без сотрудника, если всё в одной таблице;
  • аномалии удаления:
    • удалили последнего сотрудника отдела и потеряли данные об отделе.

Нормализация помогает разложить данные по таблицам так, чтобы этих проблем не было.

Первая нормальная форма (1НФ)

Требования 1НФ:

  • каждый атрибут (поле) — атомарный, неделимый (нет списков, массивов, повторяющихся групп в одном поле);
  • каждая строка — уникальна (обычно через первичный ключ, но требование уникальности — к ключу, а не ко всем значениям);
  • отсутствие "повторяющихся столбцов" вида phone1, phone2, phone3 для одной сущности.

Пример (плохо):

  • Таблица orders:
    • id
    • customer_name
    • product_ids = "1,2,3"

Так делать нельзя в 1НФ: product_ids — неатомарно.

Пример (правильно):

  • orders(id, customer_id, created_at, ...)
  • order_items(order_id, product_id, quantity)

Атомарность и разбиение повторяющихся групп — ключ к 1НФ.

Вторая нормальная форма (2НФ)

2НФ накладывается на таблицы, где:

  • есть составной первичный ключ (несколько колонок).

Требование 2НФ:

  • таблица уже в 1НФ;
  • каждый неключевой атрибут (то есть столбец, не входящий в первичный ключ) полностью функционально зависит от всего составного ключа, а не от его части.

Если атрибут зависит только от части составного ключа — нарушение 2НФ → нужно вынести в отдельную таблицу.

Пример нарушения:

Таблица order_items:

  • (order_id, product_id) — составной первичный ключ;
  • quantity;
  • order_date.

order_date зависит только от order_id, а не от (order_id, product_id) → нарушение 2НФ.

Правильная декомпозиция:

  • orders(order_id, order_date, ...)
  • order_items(order_id, product_id, quantity, PRIMARY KEY(order_id, product_id))

Идея: в таблице с составным ключом каждый неключевой столбец должен описывать конкретную комбинацию ключа целиком, а не часть.

Третья нормальная форма (3НФ)

Требования 3НФ:

  • таблица в 2НФ;
  • нет транзитивных зависимостей: неключевые атрибуты не должны зависеть друг от друга, а только от ключа.

"Транзитивная зависимость" — когда A (ключ) определяет B, а B определяет C, и C оказывается в той же таблице.

Пример нарушения:

Таблица customers:

  • id (PK)
  • city_id
  • city_name

Зависимости:

  • id → city_id
  • city_id → city_name

city_name зависит от city_id, а не напрямую от id → транзитивная зависимость → нарушение 3НФ.

Правильно:

  • customers(id, city_id, ...)
  • cities(city_id, city_name, ...)

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

BCNF (Boyce-Codd Normal Form)

Усиление 3НФ.

Неформально:

  • для каждой нетривиальной функциональной зависимости X → Y, X должен быть суперключом (ключом или надмножеством ключа).

Если в таблице есть зависимости, где детерминант (левая часть зависимости) не является ключом, — это кандидат на декомпозицию.

BCNF чаще важна при сложных функциональных зависимостях; для CRUD-систем часто достаточно 3НФ.

Практические примеры на SQL

  1. 1НФ и связь заказов и товаров:
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE order_items (
order_id BIGINT NOT NULL REFERENCES orders(id),
product_id BIGINT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
PRIMARY KEY (order_id, product_id)
);
  1. 3НФ: вынос справочника городов:
CREATE TABLE cities (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL
);

CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
city_id BIGINT REFERENCES cities(id)
);

Почему это важно для реального проекта

  • Обновляемость:
    • изменение названия города — в одном месте, а не в тысячах строк.
  • Целостность:
    • внешние ключи защищают от "висящих" ссылок;
  • Предсказуемость запросов:
    • нет дублирующих и противоречивых данных.
  • Но:
    • избыточная нормализация может усложнить чтение и увеличить количество JOIN-ов.

Баланс: нормализация vs денормализация

Зрелый подход:

  • спроектировать модель в 3НФ / BCNF;
  • затем при необходимости (под нагрузкой чтения):
    • точечно денормализовать;
    • добавить кэш-таблицы, материализованные представления;
    • хранить агрегаты (likes_count, comments_count и т.п.) отдельно.

Всегда помнить:

  • денормализация — осознанная оптимизация поверх корректной нормализованной модели;
  • а не "сразу всё в одну таблицу, чтобы проще было запрос писать".

Краткое резюме для собеседования:

  • Нормализация — это про устранение дублирования и аномалий.
  • Минимум нужно уверенно знать:
    • 1НФ: атомарные значения, отсутствие повторяющихся групп.
    • 2НФ: для составных ключей — полная зависимость от всего ключа.
    • 3НФ: нет транзитивных зависимостей; неключевые атрибуты не зависят друг от друга.
  • Понимать BCNF как усиление 3НФ.
  • Уметь привести 1–2 практических примера с разбиением таблицы.

Вопрос 21. С какими индексами в базе данных ты работал и всегда ли использование индексов является хорошей практикой?

Таймкод: 00:15:42

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

Правильный ответ:

Индекс в СУБД — это вспомогательная структура данных (обычно B-дерево или его вариация), которая ускоряет поиск строк по значениям одного или нескольких столбцов. Индексы — критический инструмент для производительности, но их использование всегда компромисс между скоростью чтения и стоимостью записи/хранения.

Важно уметь:

  • называть основные типы индексов (для PostgreSQL/MySQL и т.п.);
  • понимать, как они работают концептуально;
  • объяснять, когда индекс помогает, а когда вреден.

Основные виды индексов (на практике)

  1. Индекс по первичному ключу (PRIMARY KEY)
  • Гарантирует уникальность и не NULL.
  • В большинстве СУБД (PostgreSQL, MySQL/InnoDB) реализуется как B-tree UNIQUE INDEX.
  • Часто кластеризованный индекс (в InnoDB данные таблицы физически отсортированы по PK).
  • Используется:
    • для поиска по id;
    • для связей по внешним ключам.

Пример:

CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
  1. Обычный (неуникальный) B-tree индекс
  • Ускоряет WHERE, JOIN, ORDER BY, GROUP BY по индексируемым столбцам.
  • Подходит для точного поиска, диапазонов, сортировок.

Пример:

CREATE INDEX idx_documents_created_at ON documents (created_at);
CREATE INDEX idx_documents_title ON documents (title);
  1. Уникальный индекс (UNIQUE)
  • Гарантирует уникальность значений (или набора значений).
  • Важно для бизнес-ограничений: email пользователя, (post_id, user_id) для лайков.

Пример:

CREATE UNIQUE INDEX ux_users_email ON users (email);

CREATE TABLE post_likes (
post_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
PRIMARY KEY (post_id, user_id) -- фактически UNIQUE индекс
);
  1. Составной индекс (composite index)
  • Индекс по нескольким колонкам.
  • Важно понимать порядок полей.

Пример:

CREATE INDEX idx_orders_customer_date
ON orders (customer_id, created_at);

Как это работает:

  • Запрос WHERE customer_id = ? AND created_at >= ? эффективно использует индекс.
  • Запрос только по customer_id тоже может использовать.
  • Запрос только по created_at — уже нет (для B-tree порядок важен).
  1. Частичные (partial) индексы (PostgreSQL)
  • Индекс по подмножеству строк.

Пример:

CREATE INDEX idx_documents_active
ON documents (created_at)
WHERE status = 'active';

Плюсы:

  • меньше размер;
  • быстрее вставки/обновления;
  • фокус на реально нужных данных.
  1. Индексы по выражениям (expression / functional indexes)
  • Индекс по результату функции/выражения.

Пример:

CREATE INDEX idx_users_lower_email
ON users (lower(email));

Позволяет эффективно выполнять:

SELECT * FROM users WHERE lower(email) = lower($1);
  1. Специализированные индексы

В PostgreSQL:

  • GIN/GiST:
    • для полнотекстового поиска, JSONB, массивов, геоданных.
  • BRIN:
    • для очень больших таблиц, где данные по столбцу коррелируют с физическим порядком строк (time-series).

Примеры:

-- Полнотекстовый поиск
CREATE INDEX idx_docs_fts
ON documents USING GIN (to_tsvector('russian', content));

-- JSONB
CREATE INDEX idx_events_payload
ON events USING GIN (payload jsonb_path_ops);

В MySQL:

  • FULLTEXT индексы;
  • Spatial индексы.

Почему индексы не всегда хороши

Индекс — не "бесплатное ускорение". Есть издержки:

  1. Замедление операций записи
  • При INSERT:
    • нужно не только вставить строку в таблицу, но и обновить все индексы.
  • При UPDATE:
    • если изменяется индексируемый столбец, нужно перестроить записи в индексе.
  • При DELETE:
    • удалить записи из индексов.

Чем больше индексов — тем дороже insert/update/delete.

  1. Дополнительное потребление памяти/диска
  • Индексы занимают место.
  • Большое количество или тяжёлые индексы:
    • увеличивают размер базы;
    • хуже помещаются в память;
    • могут замедлять работу из-за роста I/O.
  1. Плохая селективность

Селективность — насколько столбец "различает" строки.

  • Если индекс по полю, где почти все значения одинаковые (status='active' для 99% строк):
    • СУБД может решить не использовать индекс, а сделать seq scan;
    • такой индекс мало полезен, но создаёт накладные расходы.
  • Хорошие кандидаты для индекса:
    • поля с высокой кардинальностью (много уникальных значений): id, email, created_at, user_id в некоторых контекстах.
  1. Неверные/избыточные индексы
  • Дублирующие индексы:
    • INDEX (a) и INDEX (a, b) — первый часто лишний.
  • Индексы по полям, которые не участвуют ни в фильтрации, ни в сортировке, ни в join — лишние.
  • Составной индекс с неправильным порядком полей:
    • приводит к тому, что запросы не используют индекс, хотя он есть.

Когда индекс — хорошая практика

  • Часто используемые фильтры в WHERE:
    • WHERE email = ?, WHERE user_id = ?, WHERE created_at >= ?.
  • Колонки, по которым выполняются JOIN:
    • внешние ключи.
  • Поля для сортировки и пагинации:
    • ORDER BY created_at, ORDER BY id с фильтром.
  • Уникальные бизнес-ограничения:
    • email, логин, (post_id, user_id) и т.д.

Когда нужно подумать дважды

  • Частые массовые вставки/обновления:
    • лог-таблицы, события, метрики;
    • лишние индексы сильно режут throughput.
  • Низкоселективные поля:
    • status, флаги is_active, если почти все значения одинаковы.
  • Много индексов на маленькой таблице:
    • индекс не даёт выигрыша, таблица и так помещается в памяти.

Практический подход

  1. Стартовать с:
  • PK;
  • индексов по внешним ключам;
  • индексов под ключевые запросы (по WHERE и JOIN).
  1. Использовать EXPLAIN/EXPLAIN ANALYZE:
  • смотреть, используют ли запросы индексы;
  • отслеживать seq scan-ы там, где они нежелательны.
  1. Регулярно ревизовать индексы:
  • искать неиспользуемые (в PostgreSQL есть pg_stat_user_indexes);
  • удалять лишние.

Пример: индекс для лайков и запросов

CREATE TABLE post_likes (
post_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (post_id, user_id)
);

-- Уже есть эффективный индекс по (post_id, user_id)
-- Запросы:
-- - проверить наличие лайка: WHERE post_id = ? AND user_id = ?
-- - посчитать лайки по посту: WHERE post_id = ?

Здесь:

  • составной PK полностью покрывает типичные запросы;
  • дополнительный индекс только по post_id, как правило, не нужен.

Краткое резюме для собеседования:

  • Да, работал:
    • с PK, UNIQUE, обычными и составными индексами;
    • понимаю partial/functional индексы (особенно в PostgreSQL).
  • Индексы полезны, когда:
    • улучшают планы выполнения частых запросов по фильтрации/джоинам/сортировке;
    • соответствуют селективным условиям.
  • Индексы не всегда хороши:
    • замедляют вставки/обновления/удаления;
    • занимают память/диск;
    • при плохой селективности или неправильном дизайне не используются оптимизатором.
  • Всегда опираюсь на:
    • конкретные запросы,
    • метрики,
    • EXPLAIN ANALYZE,
    • и минимально необходимый набор индексов под реальные паттерны нагрузки.

Вопрос 22. Какие типы JOIN и UNION ты знаешь и что используешь чаще всего?

Таймкод: 00:16:54

Ответ собеседника: правильный. Перечисляет INNER JOIN, LEFT JOIN, RIGHT JOIN, OUTER JOIN и UNION; отмечает, что чаще всего использует INNER JOIN.

Правильный ответ:

Важны не только перечисление, но и чёткое понимание семантики каждого типа JOIN, отличий между ними, а также разницы между JOIN и UNION. Это критично при проектировании запросов к PostgreSQL/MySQL и при оптимизации.

Основные типы JOIN

  1. INNER JOIN
  • Возвращает только те строки, для которых условие соединения выполняется в обеих таблицах.
  • Семантика: пересечение множеств по условию.

Пример: документы и пользователи, создавшие их:

SELECT d.id, d.title, u.name AS author
FROM documents d
INNER JOIN users u ON d.user_id = u.id;

Здесь в результат попадут только документы, у которых есть соответствующий пользователь.

  1. LEFT JOIN (LEFT OUTER JOIN)
  • Возвращает все строки из левой таблицы (перед JOIN), даже если в правой нет совпадений.
  • Для "несовпавших" строк поля правой таблицы будут NULL.
  • Семантика: "все из левой + данные справа, если есть".

Пример: все документы + информация об авторе, если он найден:

SELECT d.id, d.title, u.name AS author
FROM documents d
LEFT JOIN users u ON d.user_id = u.id;

Если пользователь удалён или отсутствует, документ всё равно будет в выборке, author = NULL.

  1. RIGHT JOIN (RIGHT OUTER JOIN)
  • Аналог LEFT JOIN, но симметрично:
    • все строки из правой таблицы;
    • совпадения из левой, если есть;
    • при отсутствии совпадений — поля левой таблицы NULL.
  • Используется реже; чаще предпочитают LEFT JOIN, меняя порядок таблиц.

Пример (эквивалент предыдущему, но "с ног на голову"):

SELECT d.id, d.title, u.name AS author
FROM users u
RIGHT JOIN documents d ON d.user_id = u.id;
  1. FULL OUTER JOIN
  • Возвращает:
    • все строки, для которых есть совпадения;
    • плюс все строки, которым не нашлось пары ни слева, ни справа.
  • Для "односторонних" строк недостающая часть будет NULL.
  • Полезен для аналитики, сверок данных.

Пример (PostgreSQL):

SELECT a.id AS a_id, b.id AS b_id
FROM table_a a
FULL OUTER JOIN table_b b ON a.key = b.key;
  1. CROSS JOIN
  • Декартово произведение: каждая строка левой таблицы с каждой строкой правой.
  • Обычно явно не нужен, но иногда применим для генерации комбинаций, календарей и т.п.
SELECT *
FROM users u
CROSS JOIN roles r;

INNER vs OUTER JOIN: важный момент

  • INNER JOIN:
    • фильтрует строки — берёт только те, где есть соответствие.
  • LEFT/RIGHT/FULL OUTER JOIN:
    • сохраняют строки одной или двух таблиц, даже если пар нет.

Типичные практики:

  • В прикладной разработке чаще всего:
    • INNER JOIN — когда нужны только "валидные/связанные" данные;
    • LEFT JOIN — когда "главная" сущность обязательна, а связанные — опциональны (например, документ с опциональным комментарием/метаданными);
  • RIGHT JOIN и FULL OUTER JOIN — реже, чаще в отчётности и миграциях.

UNION и его виды

Важно: UNION — это не JOIN.

  • JOIN:
    • объединяет строки по столбцам разных таблиц "по горизонтали";
    • добавляет столбцы (расширяет ширину строки).
  • UNION:
    • объединяет результаты нескольких SELECT "по вертикали";
    • добавляет строки, при этом количество и типы столбцов в запросах должны совпадать.
  1. UNION
  • Объединяет результаты нескольких SELECT.
  • Убирает дубликаты (по всем столбцам).
  • Логически: оператор "OR" по наборам строк.
SELECT email FROM users_active
UNION
SELECT email FROM users_legacy;
  1. UNION ALL
  • То же, что UNION, но не убирает дубликаты.
  • Обычно быстрее и предпочтительнее, если устранение дублей не требуется.
SELECT email FROM users_active
UNION ALL
SELECT email FROM users_legacy;

Когда что использовать

  • INNER JOIN:
    • "Дай только те документы, у которых есть валидный владелец."
  • LEFT JOIN:
    • "Дай все документы, даже если владелец уже удалён."
  • FULL OUTER JOIN:
    • "Покажи расхождения между двумя источниками данных."
  • UNION:
    • "Объедини результаты двух выборок как множество (без дублей)."
  • UNION ALL:
    • "Сложи два набора строк как есть (с дублями), часто для логов, событий, выборки из шардов."

Пример практического запроса (документооборот)

Все документы и, если есть, дата последнего комментария:

SELECT d.id,
d.title,
MAX(c.created_at) AS last_comment_at
FROM documents d
LEFT JOIN comments c ON c.document_id = d.id
GROUP BY d.id, d.title;

Здесь LEFT JOIN важен:

  • документы без комментариев тоже должны быть в выдаче;
  • при INNER JOIN они бы пропали.

Краткое резюме для собеседования:

  • Знаю и использую:
    • JOIN: INNER, LEFT (OUTER), RIGHT, FULL OUTER, CROSS;
    • UNION и UNION ALL, понимаю разницу (distinct vs все строки).
  • Чаще всего:
    • INNER JOIN для "жёстких" связей;
    • LEFT JOIN для обязательной основной сущности и опциональных связей;
    • UNION ALL для объединения потоков данных.
  • Осознанно выбираю тип соединения исходя из бизнес-семантики:
    • не только "чтобы работало", но чтобы корректно отражало обязательность/опциональность связей и не теряло данные.

Вопрос 23. Как можно получить данные из нескольких таблиц без использования JOIN, если есть такое требование?

Таймкод: 00:17:18

Ответ собеседника: неполный. Упоминает UNION при совпадении структуры столбцов и подзапросы/CTE, но не показывает чёткого понимания, как именно ими заменить JOIN в типичных задачах.

Правильный ответ:

Формулировка "без использования JOIN" встречается редко и обычно означает одно из двух:

  • либо техническое/учебное ограничение ("реши задачу без явного JOIN");
  • либо бизнес-/архитектурное требование вытащить данные из нескольких источников и объединить их на уровне приложения или другого слоя.

Важно понимать:

  1. JOIN — это не "магия", а декларативная форма соединения множеств по условию.
  2. Аналогичное поведение можно получить другими способами:
    • подзапросы (subquery);
    • CTE (WITH);
    • UNION / UNION ALL (но только для вертикального объединения одинаковых структур);
    • обработка на уровне приложения.

Разберём по сути.

  1. Подзапросы вместо JOIN

Частый приём: достать данные из одной таблицы, а связанные значения взять через подзапросы. Это логически близко к JOIN, но синтаксически без ключевого слова JOIN.

Пример задачи: нужно вывести список документов с именем автора из таблицы users.

Классический вариант с JOIN:

SELECT d.id,
d.title,
u.name AS author_name
FROM documents d
JOIN users u ON u.id = d.user_id;

Вариант без явного JOIN: коррелированный подзапрос

SELECT d.id,
d.title,
(
SELECT u.name
FROM users u
WHERE u.id = d.user_id
) AS author_name
FROM documents d;

Особенности:

  • Семантически это почти то же самое, часто оптимизатор превращает это во внутренний JOIN.
  • Важно следить за производительностью:
    • на больших объёмах без индекса по users.id будет больно;
    • с индексом — нормально, но синтаксис менее нагляден.

Другой пример: количество лайков для каждого документа без JOIN:

SELECT d.id,
d.title,
(
SELECT count(*)
FROM post_likes pl
WHERE pl.post_id = d.id
) AS likes_count
FROM documents d;

Здесь подзапрос заменяет агрегирующий LEFT JOIN.

  1. CTE (WITH) вместо "явного" JOIN

CTE сам по себе не заменяет JOIN, но позволяет структурировать запрос. Можно:

  • сначала выбрать данные из одной таблицы;
  • затем в основном запросе соединить их через подзапросы.

Пример (условное "избегание" JOIN):

WITH docs AS (
SELECT id, title, user_id
FROM documents
WHERE created_at > now() - interval '7 days'
)
SELECT d.id,
d.title,
(
SELECT u.name
FROM users u
WHERE u.id = d.user_id
) AS author_name
FROM docs d;

Технически JOIN всё равно по сути есть (в виде подзапроса), но формально ключевое слово JOIN не используется.

  1. UNION / UNION ALL

Важно: UNION не заменяет JOIN по смыслу.

  • JOIN "склеивает по столбцам" строки из разных таблиц по условию.
  • UNION "складывает вертикально" результаты нескольких SELECT с одинаковой структурой.

UNION применим, когда:

  • нужно собрать единый список однотипных сущностей из разных таблиц.

Пример:

Есть две таблицы:

  • incoming_documents(id, title, created_at)
  • outgoing_documents(id, title, created_at)

Хотим общий список документов:

SELECT id, title, created_at, 'incoming' AS doc_type
FROM incoming_documents

UNION ALL

SELECT id, title, created_at, 'outgoing' AS doc_type
FROM outgoing_documents;

Это корректное использование UNION:

  • структуры совпадают;
  • задача — объединить потоки данных, а не связать сущности.

Но UNION не решает задачу "подтянуть автора к документу". Для связей между разными сущностями всё равно нужны либо JOIN, либо подзапросы.

  1. Обработка на стороне приложения

Если есть жёсткое требование "в SQL без JOIN", ещё один практический вариант:

  • Сделать несколько отдельных запросов.
  • Объединить данные в коде приложения (на Go, PHP и т.д.).

Пример на Go (упрощённо):

// 1. Получаем документы
rows, _ := db.QueryContext(ctx, `
SELECT id, title, user_id
FROM documents
`)
defer rows.Close()

type Document struct {
ID int64
Title string
UserID int64
}

var docs []Document
userIDs := make(map[int64]struct{})

for rows.Next() {
var d Document
rows.Scan(&d.ID, &d.Title, &d.UserID)
docs = append(docs, d)
userIDs[d.UserID] = struct{}{}
}

// 2. Получаем пользователей одним запросом по списку id
ids := make([]int64, 0, len(userIDs))
for id := range userIDs {
ids = append(ids, id)
}

query, args, _ := sqlx.In(`SELECT id, name FROM users WHERE id IN (?)`, ids)
rows2, _ := db.QueryContext(ctx, query, args...)
defer rows2.Close()

userNames := make(map[int64]string)
for rows2.Next() {
var id int64
var name string
rows2.Scan(&id, &name)
userNames[id] = name
}

// 3. Маппим
for i := range docs {
docs[i].Title = fmt.Sprintf("%s (author=%s)",
docs[i].Title,
userNames[docs[i].UserID],
)
}

Где это может иметь смысл:

  • при сложных агрегациях;
  • при необходимости кэширования;
  • при ограничениях на SQL-диалект;
  • если JOIN-ы становятся слишком тяжёлыми и выгоднее управлять выборками самому.

Но важно понимать: это осознанная архитектурная мера, а не "обход JOIN, потому что я их не знаю".

  1. Почему вопрос вообще задают

Интервьюеру важно увидеть:

  • понимаешь ли ты разницу:
    • JOIN — горизонтальное объединение по ключам;
    • UNION — вертикальное объединение результатов;
    • подзапрос — альтернатива явному JOIN, но с тем же смыслом;
  • умеешь ли мыслить множествами и вариантами реализации:
    • подтянуть данные подзапросом;
    • агрегировать отдельно (COUNT/EXISTS в подзапросах);
    • использовать несколько запросов и сшивать данные на уровне приложения;
  • не пытаешься ли "запихнуть UNION везде", где нужен JOIN.

Краткий ответ, который выглядел бы уверенно:

  • Если по каким-то причинам нельзя использовать явный JOIN:
    • применяю коррелированные подзапросы (scalar subquery, EXISTS);
    • могу использовать CTE для структурирования;
    • UNION/UNION ALL использую не вместо JOIN, а для объединения однотипных наборов строк;
    • в крайних случаях — делаю несколько запросов и объединяю данные в приложении.
  • При этом понимаю, что по сути многие подзапросы будут оптимизированы СУБД до того же плана, что и JOIN, и важно следить за производительностью.

Вопрос 24. Реализовывал ли ты пагинацию и понимаешь ли, как она работает внутри?

Таймкод: 00:17:59

Ответ собеседника: неполный. Использовал готовый KnpPaginatorBundle в Symfony, интуитивно понимает идею ограничения числа записей и сдвига выборки, после подсказок упоминает LIMIT и OFFSET, но изначально путается и не раскрывает различия между способами пагинации и их влиянием на производительность.

Правильный ответ:

Пагинация — это механизм разбиения результатов запроса на страницы, чтобы:

  • не загружать и не передавать клиенту все записи сразу;
  • уменьшить нагрузку на БД и сеть;
  • улучшить UX (быстрая выдача части данных).

В реальных системах важно понимать не только LIMIT/OFFSET, но и их влияние на производительность, а также альтернативные стратегии: keyset-пагинация, cursor-based, seek-пагинация.

Базовые подходы к пагинации

  1. LIMIT/OFFSET (классическая пагинация)

Самый простой и распространенный способ.

Пример:

SELECT id, title
FROM documents
ORDER BY id
LIMIT 20 OFFSET 40; -- страница 3, по 20 записей
  • LIMIT — сколько строк вернуть.
  • OFFSET — сколько строк пропустить.

Плюсы:

  • простой и понятный;
  • легко интегрируется с UI (page = 1,2,3…).

Минусы:

  • При больших OFFSET БД вынуждена "пролистать" много строк:
    • OFFSET 1000000 LIMIT 20 может быть очень дорогим;
    • строки до офсета всё равно читаются/считаются планировщиком.
  • Нестабильность при модификациях данных:
    • при вставках/удалениях между запросами одна и та же страница может возвращать разные записи;
    • пользователю могут "перепрыгивать" или "повторяться" строки.
  • Требует чёткого ORDER BY:
    • пагинация без детерминированной сортировки — ошибка.

Использовать:

  • для небольших объёмов и административных интерфейсов;
  • когда простота важнее идеальной эффективности.
  1. Keyset/Seek-пагинация (по значению ключа)

Более производительный способ для больших данных.

Идея:

  • вместо OFFSET использовать "последний полученный ключ";
  • запрос следующих записей формулируется как "больше (или меньше) этого значения".

Пример:

Первая страница:

SELECT id, title
FROM documents
WHERE status = 'active'
ORDER BY id
LIMIT 20;

Клиент запоминает last_id (например, 120).

Следующая страница:

SELECT id, title
FROM documents
WHERE status = 'active'
AND id > 120
ORDER BY id
LIMIT 20;

Плюсы:

  • не зависит от OFFSET;
  • масштабируется на большие таблицы;
  • использует индекс по id эффективно (index range scan).

Минусы:

  • нельзя просто "перейти на страницу 1000" по номеру страницы;
  • нужно работать с курсорами/токенами (last_id, last_created_at и т.п.).

Чаще всего это лучший выбор для API с бесконечным скроллом, списков событий, логов, документов.

  1. Cursor-based пагинация (генерализованный keyset)

Популярна в API (GitHub, GraphQL, соцсети).

Идея:

  • сервер возвращает "курсор" (часто зашифрованный/кодированный), описывающий позицию (ключи сортировки);
  • клиент в следующем запросе передаёт этот курсор;
  • сервер использует его для формирования WHERE.

Пример концептуально:

Ответ:

{
"items": [ ... ],
"next_cursor": "id:120"
}

Следующий запрос:

GET /documents?cursor=id:120&limit=20

На SQL уровне:

SELECT id, title
FROM documents
WHERE id > 120
ORDER BY id
LIMIT 20;

Преимущества:

  • детерминированность;
  • хорошая производительность;
  • удобство для API.
  1. Комбинированный подход: LIMIT/OFFSET + индекс

Для умеренных данных можно сильно улучшить ситуацию, корректно прописав ORDER BY по индексируемому столбцу.

Пример:

CREATE INDEX idx_documents_created_at_id ON documents (created_at, id);

SELECT id, title, created_at
FROM documents
WHERE status = 'active'
ORDER BY created_at, id
LIMIT 20 OFFSET 1000;
  • При правильном плане БД использует индекс и может частично оптимизировать пропуск.
  • Но фундаментальная проблема больших OFFSET остаётся.

Практические моменты, которые важно понимать

  1. Пагинация ВСЕГДА должна быть вместе с ORDER BY.

Плохой пример:

SELECT * FROM documents LIMIT 20 OFFSET 40;
  • порядок строк не гарантирован → страницы "прыгают".

Правильный пример:

SELECT id, title
FROM documents
ORDER BY id
LIMIT 20 OFFSET 40;
  1. Пагинация по первичному ключу

Если есть автоинкрементный/монотонный id, seek-пагинация по нему:

  • простая;
  • быстрая.
  1. Пагинация по дате/сложному ключу

Если сортируем по created_at, важно:

  • иметь индекс (created_at, id) для устойчивого порядка;
  • в курсоре хранить и created_at, и id.

Пример:

SELECT id, title, created_at
FROM documents
WHERE (created_at, id) > ($last_created_at, $last_id)
ORDER BY created_at, id
LIMIT 20;
  1. Ограничения готовых библиотек

Используя KnpPaginatorBundle или аналоги:

  • важно понимать, что внутри чаще всего используется LIMIT/OFFSET;
  • на больших объёмах страдает производительность;
  • при росте нагрузок нужно уметь перейти на keyset/cursor подход, реализовав логику явно.

Пример реализации пагинации на Go (LIMIT/OFFSET)

type ListDocumentsParams struct {
Limit int
Offset int
}

func (s *Store) ListDocuments(ctx context.Context, p ListDocumentsParams) ([]Document, error) {
if p.Limit <= 0 || p.Limit > 1000 {
p.Limit = 50
}

rows, err := s.db.QueryContext(ctx, `
SELECT id, title, created_at
FROM documents
ORDER BY id
LIMIT $1 OFFSET $2
`, p.Limit, p.Offset)
if err != nil {
return nil, err
}
defer rows.Close()

var docs []Document
for rows.Next() {
var d Document
if err := rows.Scan(&d.ID, &d.Title, &d.CreatedAt); err != nil {
return nil, err
}
docs = append(docs, d)
}
return docs, rows.Err()
}

Пример keyset-пагинации на Go

type ListDocumentsByCursorParams struct {
Limit int
AfterID int64 // last seen id
}

func (s *Store) ListDocumentsByCursor(ctx context.Context, p ListDocumentsByCursorParams) ([]Document, error) {
if p.Limit <= 0 || p.Limit > 1000 {
p.Limit = 50
}

rows, err := s.db.QueryContext(ctx, `
SELECT id, title, created_at
FROM documents
WHERE id > $1
ORDER BY id
LIMIT $2
`, p.AfterID, p.Limit)
if err != nil {
return nil, err
}
defer rows.Close()

var docs []Document
for rows.Next() {
var d Document
if err := rows.Scan(&d.ID, &d.Title, &d.CreatedAt); err != nil {
return nil, err
}
docs = append(docs, d)
}
return docs, rows.Err()
}

Краткое резюме, которое ожидают от сильного специалиста:

  • Понимаю, что пагинация — это не только "LIMIT/OFFSET", но и:
    • обязательный ORDER BY;
    • влияние на производительность при больших OFFSET;
    • нестабильность результатов при изменении данных.
  • Знаю и использую:
    • LIMIT/OFFSET для простых кейсов;
    • keyset/cursor-based пагинацию для больших списков и API;
  • Умею:
    • объяснить, как это связано с индексами и планами выполнения;
    • реализовать пагинацию на уровне SQL и кода (Go, PHP) без слепой зависимости от библиотек.

Вопрос 25. Что делает оператор LIMIT и что делает оператор OFFSET при организации пагинации?

Таймкод: 00:20:01

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

Правильный ответ:

Операторы LIMIT и OFFSET используются для базовой реализации пагинации в SQL-запросах.

Кратко:

  • LIMIT N:
    • ограничивает максимальное количество строк, возвращаемых запросом;
    • часто соответствует "размеру страницы" (page size).
  • OFFSET K:
    • пропускает первые K строк результата, и только затем возвращает строки (с учётом LIMIT);
    • используется для сдвига "страниц" (page 2, 3, 4 и т.д.).

Типичный пример:

-- Страница 1 (page=1), по 20 записей
SELECT id, title
FROM documents
ORDER BY id
LIMIT 20 OFFSET 0;

-- Страница 2 (page=2), по 20 записей
SELECT id, title
FROM documents
ORDER BY id
LIMIT 20 OFFSET 20;

-- Страница 3 (page=3), по 20 записей
SELECT id, title
FROM documents
ORDER BY id
LIMIT 20 OFFSET 40;

Ключевые моменты, которые важно понимать:

  • Всегда использовать LIMIT/OFFSET вместе с детерминированным ORDER BY:
    • без ORDER BY порядок строк не гарантируется;
    • пагинация без фиксированной сортировки логически некорректна.
  • OFFSET определяет, сколько строк "отбросить" от начала упорядоченного набора, LIMIT — сколько взять после этого.
  • При больших OFFSET (десятки/сотни тысяч) запросы становятся тяжёлыми:
    • СУБД всё равно должна "пролистать" отброшенные строки;
    • поэтому для больших данных предпочтительнее keyset/cursor-пагинация (по id/created_at и т.п.), а не голый OFFSET.

Но для простых и средних по объёму задач LIMIT + OFFSET — валидный и широко используемый механизм пагинации.

Вопрос 26. Знаком ли ты с шаблонами проектирования и какие можешь перечислить?

Таймкод: 00:21:39

Ответ собеседника: правильный. Указывает на знание порождающих, структурных и поведенческих паттернов; приводит примеры: Singleton, Prototype, Factory, Proxy, Bridge, Decorator, State, Command, Strategy.

Правильный ответ:

Шаблоны проектирования — это типовые, проверенные временем решения распространённых архитектурных задач. Их цель — не "усложнить код", а:

  • повысить предсказуемость и читаемость;
  • уменьшить связность;
  • упростить тестирование;
  • облегчить масштабирование и изменение системы.

Важно не просто перечислять паттерны, а понимать:

  • какую проблему каждый решает;
  • в каких сценариях он уместен;
  • как его выразить в современном стеке (Go, HTTP, БД, микросервисы), а не только в "классическом ООП".

Ниже — структурировано и прикладно, без полного каталога, но с фокусом на реально полезных.

Порождающие паттерны

  1. Factory Method / Simple Factory

Проблема:

  • создание объектов с разной конфигурацией/типами без "if-else" по всему коду.

Идея:

  • вынести логику создания в отдельную функцию/тип.

Пример (Go): фабрика HTTP-клиента под разные сервисы:

type ServiceClient struct {
baseURL string
client *http.Client
}

func NewServiceClient(baseURL string, timeout time.Duration) *ServiceClient {
return &ServiceClient{
baseURL: baseURL,
client: &http.Client{
Timeout: timeout,
},
}
}

Фабрика инкапсулирует детали: таймауты, базовый URL, настройки транспорта.

  1. Abstract Factory

Проблема:

  • нужно создавать семейства связанных объектов, которые должны согласованно работать вместе (например, разные окружения, провайдеры).

Идея:

  • набор фабрик для разных реализаций: PostgreSQL/MySQL, локальный/облачный сторедж и т.д.

Практический кейс:

  • конфигурация приложения выбирает один из "наборов" зависимостей (репозитории, клиенты, очереди) в зависимости от окружения.
  1. Builder

Проблема:

  • сложная инициализация объекта с множеством опциональных параметров.

Идея:

  • пошаговая сборка с явным API.

В Go часто решается "functional options" и структурными литералами, но принципы те же.

  1. Singleton

Проблема:

  • нужна одна-единственная точка доступа (например, конфигурация, пул подключения).

Опасность:

  • глобальное состояние, сложно тестировать, скрытые зависимости.

Зрелый подход:

  • избегать "жёсткого" Singleton-а;
  • передавать зависимости явно (DI);
  • использовать sync.Once для ленивой инициализации, когда это реально инфраструктурно оправдано.

Структурные паттерны

  1. Adapter

Проблема:

  • нужно подружить несовместимые интерфейсы.

Идея:

  • обёртка над внешней библиотекой/сервисом, приводящая его к нашему доменному интерфейсу.

Пример (Go):

type PaymentProvider interface {
Charge(amount int64, currency, customerID string) error
}

type StripeClient struct { /* ... */ }

func (c *StripeClient) Charge(amount int64, currency, customerID string) error {
// адаптация к API Stripe
return nil
}
  1. Facade

Проблема:

  • сложная подсистема, много шагов, хочется простой входной интерфейс.

Идея:

  • один "фасадный" сервис скрывает детали (например, создание документа + права + логирование + уведомления).
  1. Decorator

Проблема:

  • нужно добавить к объекту поведение (логирование, кэш, метрики) без изменения его кода.

Идея:

  • оборачиваем реализацию в другую, реализующую тот же интерфейс.

Пример (Go, декоратор репозитория):

type UserRepo interface {
GetByID(ctx context.Context, id int64) (*User, error)
}

type LoggingUserRepo struct {
next UserRepo
}

func (r *LoggingUserRepo) GetByID(ctx context.Context, id int64) (*User, error) {
start := time.Now()
u, err := r.next.GetByID(ctx, id)
log.Printf("GetByID id=%d took=%s err=%v", id, time.Since(start), err)
return u, err
}
  1. Proxy

Похоже на Decorator, но акцент на управлении доступом, ленивой загрузке, кэшировании, remote calls.

Пример:

  • HTTP-клиент к удалённому сервису с кэшем и ретраями, скрывающий все детали от доменного кода.
  1. Bridge

Проблема:

  • нужно независимо варьировать "абстракцию" и "реализацию".

Пример:

  • интерфейс хранения файлов (S3/локально), который используется документ-сервисом;
  • DocumentService не знает и не зависит от конкретной реализации.

Поведенческие паттерны

  1. Strategy

Проблема:

  • есть разные алгоритмы/политики (валидация, выбор тарифа, расчёт цены), переключаемые в рантайме.

Идея:

  • интерфейс + множество реализаций.

Пример (Go):

type PasswordHasher interface {
Hash(pw string) (string, error)
Verify(pw, hash string) bool
}

Разные реализации: bcrypt, argon2, scrypt.

  1. Command

Проблема:

  • нужно представить действие как объект: логирование, очередь, undo/redo.

Идея:

  • инкапсулировать запрос (команду) в объект.

Практический use-case:

  • задачи в очереди (job): сериализуем данные команды и обрабатываем асинхронно.
  1. Observer / Publisher-Subscriber

Проблема:

  • при изменении одной сущности уведомить несколько независимых обработчиков.

Идея:

  • подписчики (listeners) подписаны на события;
  • при событии им рассылается нотификация.

В реальных системах:

  • реализуется через message broker (Kafka, RabbitMQ, NATS);
  • или через доменные event-ы внутри сервиса.
  1. State

Проблема:

  • сложная логика состояний с переходами (workflow документа, статус заказа).

Идея:

  • объект состояния + чёткие переходы вместо "лесенки if-else".

Пример:

  • документ: draft → on_review → approved → signed → archived;
  • каждый переход валидируется и имеет свои правила.
  1. Template Method, Chain of Responsibility и др.
  • Template Method:
    • общий алгоритм в базовом типе + вариативные шаги в наследниках.
  • Chain of Responsibility:
    • цепочка обработчиков (middleware в HTTP, interceptors).

В Go chain of responsibility очень естественно выражается через middleware над http.Handler.

Почему это важно именно для backend / Go / highload

  • Паттерны — это не только "книжка GoF", а язык общения:
    • "здесь декоратор" сразу объясняет идею логирования/кэша;
    • "здесь адаптер" — понятно, что скрывается внешний API.
  • В Go нет классического наследования, поэтому:
    • делается упор на композицию, интерфейсы и функции;
    • большинство структурных и поведенческих паттернов выражаются проще и чище.

Что стоит показать на интервью:

  • Знаю категории: порождающие, структурные, поведенческие.
  • Могу уверенно объяснить и применить на практике:
    • Strategy, Factory, Adapter, Decorator, Proxy, Repository/Service (как архитектурные паттерны), Pub/Sub, CQRS (по необходимости).
  • Понимаю:
    • паттерны — решение конкретных проблем, а не самоцель;
    • избыточное или формальное применение паттернов вредит;
    • предпочитаю простые, читаемые решения и применяю паттерн тогда, когда он естественно описывает задачу.

Вопрос 27. Приходилось ли тебе реализовывать какие-либо шаблоны проектирования на практике?

Таймкод: 00:22:18

Ответ собеседника: неполный. Не приводит конкретных примеров; расплывчато упоминает поведенческие паттерны и выбор метода в зависимости от ситуации, без привязки к чётким шаблонам и реальным кейсам.

Правильный ответ:

На такой вопрос важно показать не теорию, а практическое применение паттернов в реальных задачах: где именно, зачем, что это дало. Хороший ответ — это 2–4 конкретных кейса с чётким мэппингом на паттерны.

Ниже примеры из типичных backend-сценариев (их можно адаптировать под свой опыт).

Паттерн Strategy: выбор различающегося поведения

Задача: разный способ расчёта чего-либо (скидок, уведомлений, маршрутизации, валидации) в зависимости от контекста.

Пример (Go): разные алгоритмы хеширования пароля.

type PasswordHasher interface {
Hash(password string) (string, error)
Verify(password, hash string) bool
}

type BcryptHasher struct{}

func (BcryptHasher) Hash(password string) (string, error) {
b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(b), err
}

func (BcryptHasher) Verify(password, hash string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}

type Argon2Hasher struct {
// параметры Argon2
}

// Реализация Argon2Hasher...

// Выбор стратегии, например, из конфига:
func NewPasswordHasher(algo string) PasswordHasher {
switch algo {
case "argon2":
return Argon2Hasher{/*...*/}
default:
return BcryptHasher{}
}
}

Что это даёт:

  • изолированы детали реализации;
  • легко сменить алгоритм или протестировать;
  • бизнес-логика не знает, как именно считается хеш.

Паттерн Repository + интерфейсы (структурный/архитектурный)

Задача: отделить доменную логику от деталей хранения (PostgreSQL, in-memory, внешнее API).

Пример (Go):

type Document struct {
ID int64
Title string
}

type DocumentRepository interface {
GetByID(ctx context.Context, id int64) (*Document, error)
Save(ctx context.Context, doc *Document) error
}

type PgDocumentRepository struct {
db *sql.DB
}

func (r *PgDocumentRepository) GetByID(ctx context.Context, id int64) (*Document, error) {
var d Document
err := r.db.QueryRowContext(ctx,
`SELECT id, title FROM documents WHERE id = $1`, id,
).Scan(&d.ID, &d.Title)
if err == sql.ErrNoRows {
return nil, nil
}
return &d, err
}

func (r *PgDocumentRepository) Save(ctx context.Context, doc *Document) error {
return r.db.QueryRowContext(ctx,
`INSERT INTO documents (title) VALUES ($1) RETURNING id`,
doc.Title,
).Scan(&doc.ID)
}

Использование в сервисе:

type DocumentService struct {
repo DocumentRepository
}

func (s *DocumentService) CreateDocument(ctx context.Context, title string) (*Document, error) {
doc := &Document{Title: title}
if err := s.repo.Save(ctx, doc); err != nil {
return nil, err
}
return doc, nil
}

Паттерны здесь:

  • Repository — инкапсулирует SQL/ORM;
  • Strategy/Adapter — можно подставить любую реализацию repo (mock для тестов, другая БД).

Паттерн Decorator: логирование, кэш, метрики

Задача: добавить нефункциональное поведение (логирование, мониторинг, кэширование) без изменения основной реализации.

Пример (Go): декоратор для DocumentRepository:

type LoggingDocumentRepository struct {
next DocumentRepository
}

func (r *LoggingDocumentRepository) GetByID(ctx context.Context, id int64) (*Document, error) {
start := time.Now()
doc, err := r.next.GetByID(ctx, id)
log.Printf("GetByID id=%d took=%s err=%v", id, time.Since(start), err)
return doc, err
}

Подключение:

pgRepo := &PgDocumentRepository{db: db}
repo := &LoggingDocumentRepository{next: pgRepo}
service := &DocumentService{repo: repo}

Что это даёт:

  • легко включать/выключать логирование;
  • основной код репозитория не захламлён технодеталями;
  • соответствует идее Open/Closed: расширили поведение без изменения исходной реализации.

Паттерн Adapter: интеграция с внешними сервисами

Задача: внешний API (SOAP/REST/gRPC) имеет неудобный или нестабильный интерфейс; хотим единый внутренний контракт.

Пример: адаптер платёжного провайдера под локальный интерфейс:

type PaymentGateway interface {
Charge(ctx context.Context, userID int64, amount int64, currency string) error
}

type StripeAdapter struct {
client *StripeClient // внешний SDK
}

func (a *StripeAdapter) Charge(ctx context.Context, userID int64, amount int64, currency string) error {
// маппим локальную модель на Stripe-запрос
return a.client.CreateCharge(ctx, userID, amount, currency)
}

Результат:

  • бизнес-логика работает с PaymentGateway;
  • можно заменить Stripe на другой провайдер, не трогая доменный код.

Паттерн Command / Queue: асинхронные операции

Задача: вынести тяжёлые операции (email, отчёты, интеграции) в фон.

Идея:

  • каждая задача — команда с данными;
  • команды кладутся в очередь и обрабатываются воркерами.

Условный пример структуры команды:

type SendEmailCommand struct {
To string
Subject string
Body string
}

Сериализация в JSON и отправка в RabbitMQ/Redis:

  • это практическая реализация Command + Message Queue (часто вместе с Observer/Event Sourcing паттернами).

Краткое резюме, ожидаемое на интервью:

  • Да, не просто знаю паттерны по книжке, а использовал их:
    • Strategy — для выбора алгоритмов (валидация, расчёт, интеграции).
    • Repository + интерфейсы — для отделения домена от данных и удобного тестирования.
    • Decorator/Proxy — для логирования, кэша, ретраев вокруг репозиториев и клиентов.
    • Adapter — для оберток над внешними API, чтобы внутри кода был стабильный интерфейс.
    • Command/Queue — для асинхронных задач и интеграций.
  • Осознанно выбираю паттерн под задачу:
    • не "натягиваю" их ради галочки,
    • а использую там, где они упрощают поддержку, тестирование и расширяемость системы.

Вопрос 28. Что такое шаблон Singleton и как он реализуется?

Таймкод: 00:23:03

Ответ собеседника: правильный. Описывает Singleton как шаблон, гарантирующий существование только одного экземпляра класса с приватным конструктором и статическим методом доступа, который при первом вызове создаёт объект, а затем возвращает уже созданный.

Правильный ответ:

Singleton — это шаблон проектирования, цель которого:

  • гарантировать, что в системе существует только один экземпляр определённого объекта;
  • предоставить глобальную точку доступа к этому экземпляру.

Однако важно понимать не только классическую реализацию, но и практические нюансы:

  • потокобезопасность;
  • проблемы тестируемости;
  • влияние на архитектуру;
  • уместность использования в современных сервисах (в том числе на Go).

Классическая реализация Singleton (PHP)

Ключевые элементы:

  • приватный конструктор (запрещает new извне);
  • статическое поле для хранения единственного экземпляра;
  • статический метод getInstance().

Пример:

<?php

class DbConnection
{
private static ?DbConnection $instance = null;

private \PDO $pdo;

// Запрещаем создание извне
private function __construct()
{
$this->pdo = new \PDO('pgsql:host=localhost;dbname=test', 'user', 'password');
}

// Запрещаем клонирование и unserialize (для надёжности)
private function __clone() {}
public function __wakeup()
{
throw new \Exception("Cannot unserialize singleton");
}

public static function getInstance(): DbConnection
{
if (self::$instance === null) {
self::$instance = new self();
}

return self::$instance;
}

public function getPdo(): \PDO
{
return $this->pdo;
}
}

// Использование:
$db = DbConnection::getInstance()->getPdo();

Это канонический пример, который демонстрируют на собеседованиях. Но в продакшене к нему есть серьёзные вопросы.

Проблемы классического Singleton

  1. Глобальное состояние:

    • По сути — "легальный синоним" глобальной переменной.
    • Усложняет понимание зависимостей: объекты берут зависимости "из воздуха", вместо явного DI.
    • Усложняет параллельное исполнение и изоляцию.
  2. Тестируемость:

    • Сложно подменять реализацию (моки/фейки);
    • Общий инстанс может "утекать" между тестами.
  3. Потокобезопасность:

    • В средах с многопоточностью нужна защита от гонок при ленивой инициализации.
  4. Жизненный цикл:

    • Жёстко привязан к процессу; переинициализация затруднена.

Зрелый подход:

  • использовать Singleton осознанно и в ограниченных местах инфраструктуры;
  • в прикладном коде предпочтительнее:
    • явное внедрение зависимостей (Dependency Injection);
    • управление жизненным циклом объектов на уровне composition root.

Потокобезопасный Singleton на Go (через sync.Once)

Go не имеет классов и статических методов, но паттерн "одиночка" встречается в инфраструктурных компонентах (инициализация конфигурации, логгера, подключения).

Пример:

package config

import (
"log"
"sync"
)

type Config struct {
DSN string
// другие настройки
}

var (
instance *Config
once sync.Once
)

func Get() *Config {
once.Do(func() {
// Здесь можно загрузить конфиг из файла, env, Vault и т.д.
instance = &Config{
DSN: "postgres://user:pass@localhost:5432/db",
}
log.Println("config initialized")
})
return instance
}

Ключевые моменты:

  • sync.Once гарантирует, что инициализация выполнится ровно один раз, даже при конкурирующих вызовах из разных горутин.
  • Это потокобезопасный вариант ленивого Singleton.

Но даже в Go чаще рекомендуют:

  • передавать *Config как зависимость в main:
    • NewServer(cfg *Config, db *sql.DB, logger Logger);
  • минимизировать использование глобальных синглтонов.

Когда Singleton уместен

  • Инфраструктурный код:
    • логгер по умолчанию;
    • глобальные конфиги в небольших утилитах;
    • connection pool, если его создание централизовано и неизменяемо.
  • Legacy или фреймворки, где архитектура уже навязала глобальные точки доступа.

Когда лучше не использовать

  • В доменной логике и бизнес-сервисах:
    • предпочтительнее DI и интерфейсы;
    • это повышает тестируемость, расширяемость и прозрачность зависимостей.
  • В микросервисах и высоконагруженных системах:
    • тяжёлые синглтоны могут стать скрытым bottleneck-ом;
    • лучше чётко управлять жизненным циклом и количеством экземпляров.

Кратко для собеседования:

  • Singleton — шаблон, гарантирующий один экземпляр и глобальную точку доступа.
  • Реализуется через:
    • приватный конструктор,
    • статическое поле,
    • статический метод доступа (PHP/Java-подобные языки),
    • в Go — через пакетный уровень + sync.Once.
  • Осознаю недостатки:
    • глобальное состояние, сложность тестирования, скрытые зависимости.
  • В современных сервисах стараюсь:
    • использовать Singleton только в инфраструктуре,
    • в остальном — явный DI и интерфейсы, чтобы код оставался гибким и поддерживаемым.

Вопрос 29. Применяешь ли ты на практике принципы SOLID, в частности принцип подстановки Барбары Лисков (L)?

Таймкод: 00:23:49

Ответ собеседника: правильный. Говорит, что старается применять SOLID, замечает нарушения единственной ответственности, корректно формулирует принцип Лисков как возможность замены подтипа базовым типом без изменения логики.

Правильный ответ:

Принципы SOLID — это практические ориентиры для построения устойчивой архитектуры. В контексте серверной разработки, микросервисов и Go они особенно важны, потому что помогают:

  • уменьшить связность;
  • упростить замену и развитие компонентов;
  • улучшить тестируемость;
  • избежать "хрупких" абстракций.

Кратко напомню и акцентирую на применении, особенно принципа Лисков.

SOLID (очень сжато)

  • S — Single Responsibility Principle:
    • один модуль/тип — одна осмысленная причина для изменения.
  • O — Open/Closed Principle:
    • открыто для расширения, закрыто для модификации.
  • L — Liskov Substitution Principle:
    • подтип должен полностью корректно подменять базовый тип.
  • I — Interface Segregation Principle:
    • лучше несколько маленьких интерфейсов, чем один "жирный".
  • D — Dependency Inversion Principle:
    • зависимости строятся от абстракций, а не от конкретных реализаций.

Принцип подстановки Лисков (LSP)

Формулировка по сути:

Если есть код, работающий с неким типом (абстракцией), то любая его реализация (подтип) должна:

  • быть взаимозаменяемой;
  • не ломать ожидания клиента;
  • не ослаблять инварианты и предусловия;
  • не нарушать постусловия.

Важно: это не только про ключевое слово "extends" в ООП, но и про интерфейсы, контракты и поведение.

Практические анти-примеры LSP:

  1. "Фальшивый" подтип, который не поддерживает часть контракта

Например, интерфейс:

type Storage interface {
Save(ctx context.Context, key string, data []byte) error
Load(ctx context.Context, key string) ([]byte, error)
}

Плохая реализация:

  • "WriteOnlyStorage", где Load всегда возвращает ошибку "not supported".

Формально интерфейс реализован, но:

  • любой код, который ожидает рабочий Load, сломается с этим подтипом;
  • нарушение LSP: подтип не удовлетворяет ожиданиям клиента абстракции.

Правильнее:

  • разделить интерфейсы (Interface Segregation):
    • Writer, Reader и т.д.;
  • либо не притворяться подтипом, если не можешь выполнить контракт.
  1. Изменение семантики

Например, интерфейс репозитория:

type UserRepository interface {
GetByID(ctx context.Context, id int64) (*User, error) // nil, nil — если не найден
}

Одна реализация:

  • корректно возвращает (nil, nil) когда пользователя нет.

Другая:

  • кидает ошибку в этой ситуации.

Клиентский код, написанный под контракт "nil, nil означает нет данных", ломается со второй реализацией — нарушение LSP.

Зрелая практика:

  • чётко описывать контракты;
  • следить, чтобы все реализации вели себя согласованно.
  1. Ограничение поведения подтипа

Классический теоретический пример:

  • базовый тип "Прямоугольник" с setWidth/setHeight;
  • подтип "Квадрат", который вынужден странно себя вести (меняя обе стороны).

В прикладном коде это проявляется так:

  • "расширили" тип, но ломают ожидаемое поведение базового.

Реальное применение LSP в backend-разработке

  1. Интерфейсы репозиториев и клиентов

Если есть интерфейс:

type PaymentClient interface {
Charge(ctx context.Context, userID int64, amount int64) error
}

То любая реализация (Stripe, Mock, Stub):

  • должна:
    • одинаково трактовать ошибки (например, бизнес-ошибка vs техническая);
    • не менять семантику (не беззвучно "проглатывать" неуспех, если контракт обещает ошибку).

Иначе:

  • бизнес-логика, работающая через интерфейс, начнёт вести себя по-разному в зависимости от реализации;
  • это прямое нарушение LSP.
  1. Моки/фейки в тестах

LSP особенно критичен при тестировании:

  • если mock-реализация интерфейса ведёт себя проще или иначе, чем реальная, тесты могут "зеленью" прикрывать баги;
  • хороший mock должен уважать контракт, а не просто "возвращать что-то, лишь бы компилилось".
  1. Слоистая архитектура

Когда строим:

  • transport layer (HTTP/gRPC),
  • service layer (бизнес-логика),
  • repository/clients (инфраструктура),

и зависим от абстракций (интерфейсов), LSP гарантирует:

  • можем подменить БД,
  • можем заменить внешний сервис на другой,
  • можем использовать in-memory реализацию в тестах,

без переписывания бизнес-логики.

Если же подтипы ведут себя по-разному (типа "эта реализация иногда возвращает nil вместо ошибки, хотя контракт не разрешает") — архитектура становится хрупкой.

Как это выглядит в коде (Go, корректный пример)

type DocumentRepository interface {
// Возвращает (nil, nil), если нет документа
GetByID(ctx context.Context, id int64) (*Document, error)
}

type PgDocumentRepository struct { /* ... */ }

func (r *PgDocumentRepository) GetByID(ctx context.Context, id int64) (*Document, error) {
var d Document
err := r.db.QueryRowContext(ctx,
`SELECT id, title FROM documents WHERE id = $1`, id,
).Scan(&d.ID, &d.Title)
if err == sql.ErrNoRows {
return nil, nil
}
return &d, err
}

type InMemoryDocumentRepository struct {
data map[int64]*Document
}

func (r *InMemoryDocumentRepository) GetByID(ctx context.Context, id int64) (*Document, error) {
if doc, ok := r.data[id]; ok {
return doc, nil
}
return nil, nil
}

Обе реализации:

  • согласованно обрабатывают "нет данных";
  • любая может подставляться вместо другой без изменений в сервисах;
  • это и есть соблюдение LSP.

Краткое резюме, которое стоит транслировать:

  • Да, принципы SOLID применяю осознанно, особенно:
    • S: разбиваю ответственность (отдельные сервисы, репозитории, клиенты).
    • I: проектирую маленькие целевые интерфейсы, а не "god-интерфейсы".
    • D: завишу от абстракций, в том числе в Go через интерфейсы, а не от конкретных типов.
  • Принцип Лисков понимаю не как теорию, а как:
    • требование, чтобы любая реализация интерфейса вела себя согласно заявленному контракту;
    • основу для безопасной подмены реализаций (реальная БД ↔ in-memory ↔ mock, один провайдер ↔ другой).
  • Следую ему при:
    • проектировании интерфейсов (точные контракты, ожидаемая семантика),
    • написании реализаций (без "особенных" подтипов, ломающих ожидания),
    • внедрении тестов и при рефакторинге.

Вопрос 30. Как ты работаешь с Git и что означают операции merge, rebase и cherry-pick?

Таймкод: 00:24:53

Ответ собеседника: правильный. Описывает merge как объединение двух веток, rebase как перенос коммитов поверх другой ветки для выпрямления истории, cherry-pick — как выборочный перенос отдельных изменений; отмечает, что cherry-pick использовал мало.

Правильный ответ:

Для зрелой работы с Git важно не только знать команды, но и понимать их влияние на историю, коллаборацию и процессы (code review, релизы, хотфиксы). Ниже — практическое объяснение.

Операция merge

Суть:

  • merge объединяет историю двух веток, создавая новый коммит с двумя родителями (merge-коммит).
  • История сохраняется нетронутой (non-destructive): ни один существующий коммит не переписывается.

Типичный сценарий:

  • есть основная ветка main/master;
  • есть feature-ветка feature/documents-api, ответвившаяся от main;
  • после завершения работы:
git checkout main
git pull origin main
git merge feature/documents-api
git push origin main

Результат:

  • появляется merge-коммит, включающий изменения из фичи;
  • хорошо подходит для командной работы, когда важно сохранить контекст веток и merge-историю.

Плюсы:

  • не переписывает историю → безопасно для публичных веток;
  • очевидно видно, когда и что было влито.

Минусы:

  • может "загрязнять" историю множеством merge-коммитов, особенно при частых обновлениях из main;
  • сложнее визуально отслеживать linear flow.

Рекомендация:

  • для общих веток (main, develop, release) — только merge или fast-forward;
  • использовать pull request с merge (или squash) по политике команды.

Операция rebase

Суть:

  • rebase "переписывает историю", перенося коммиты одной ветки поверх другого коммита/ветки.
  • Делает историю линейной: выглядит, как будто фича ветка изначально была основана на более свежем коммите.

Типичный сценарий (обновление feature-ветки):

git checkout feature/documents-api
git fetch origin
git rebase origin/main

Что происходит:

  • Git "снимает" ваши коммиты из feature-ветки;
  • применяет их заново поверх последнего origin/main;
  • создаёт новые коммиты с новыми hash-ами.

Плюсы:

  • аккуратная, линейная история;
  • удобнее читать лог, bisect, смотреть diff.

Минусы:

  • переписывание истории:
    • если сделать rebase уже "разделённой" ветки (которую кто-то успел забрать), возникнут конфликты и рассинхрон;
    • нельзя бездумно rebase-ить публичные ветки, по которым уже работают другие.

Золотое правило:

  • rebase использовать локально для своих feature-веток до того, как они попали в общий репозиторий (или до merge);
  • не rebase-ить main / release / чужие общие ветки.

Практический пример (типичный workflow):

  • разработчик:
    • делает git rebase origin/main в своей feature-ветке, чтобы разрешить конфликты у себя;
    • пушит либо с --force-with-lease (осторожно!) после согласования политики;
    • создаёт чистый PR.

Операция cherry-pick

Суть:

  • cherry-pick копирует один (или несколько) конкретных коммитов на текущую ветку, создавая новые коммиты с теми же изменениями, но другим hash.

Зачем:

  • вытащить нужный фикс из одной ветки в другую без полного merge.
  • типичный кейс:
    • есть main и release/1.2;
    • баг пофиксили в main;
    • нужно тот же фикс срочно вкатить в release/1.2, не подтягивая всё, что накопилось в main.

Пример:

git checkout release/1.2
git cherry-pick <commit_hash>
git push origin release/1.2

Плюсы:

  • точечный перенос изменений;
  • удобно для хотфиксов, бэкпортов.

Минусы:

  • дублирование истории (один и тот же логический фикс — разные коммиты);
  • риск конфликтов;
  • нужно внимательно следить, чтобы не cherry-pick-ать одно и то же несколько раз.

Практический контекст использования

Типичный командный процесс:

  • Работа с feature-ветками:

    • ответвляем от main: feature/...;
    • коммитим мелкими логическими шагами;
    • периодически делаем git fetch + git rebase origin/main (для чистой истории);
    • создаём PR;
    • вливаем через merge или squash по политике.
  • Хотфиксы:

    • фиксим баг в отдельной ветке от релизной;
    • вливаем в релиз и затем в main;
    • или cherry-pick-ом дотягиваем фикс между ветками.
  • Rebase vs Merge в реальных командах:

    • rebase:
      • для локального выпрямления истории;
      • перед merge/squash.
    • merge:
      • для объединения долгоживущих веток;
      • в публичных ветках, чтобы не ломать историю коллег.

Ключевые моменты, которые важно доносить на интервью:

  • Понимаю:
    • merge — объединяет истории, не переписывая их;
    • rebase — переписывает историю, перенося коммиты, используется для линейной истории и локальных веток;
    • cherry-pick — выборочно переносит конкретные коммиты.
  • Умею:
    • аккуратно использовать rebase и force-push в личных/фичевых ветках;
    • не rebase-ить общие ветки;
    • применять cherry-pick для хотфиксов и бэкпортов.
  • В работе:
    • придерживаюсь принятого в команде workflow (Git Flow, Trunk-based, GitHub Flow);
    • слежу за чистотой истории и минимизацией конфликтов.

Вопрос 31. Есть ли у тебя опыт тестирования кода?

Таймкод: 00:26:14

Ответ собеседника: неполный. Упоминает минимальный опыт: несколько assert-ов без системного подхода, без ясного понимания видов тестов, практик и инструментов.

Правильный ответ:

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

  • какие виды тестов бывают;
  • как проектировать код так, чтобы его было просто тестировать;
  • как применять это в привычном стеке: HTTP-сервисы, БД, очереди, Go/PHP.

Основные виды тестов (кратко и по делу)

  1. Unit-тесты
  • Проверяют отдельные функции/методы в изоляции.
  • Не ходят в реальные внешние ресурсы (БД, HTTP, файлы) — эти зависимости мокаются/подменяются.
  • Цель:
    • быстро, дёшево, массово проверять бизнес-логику и пограничные кейсы.
  1. Интеграционные тесты
  • Проверяют взаимодействие нескольких компонентов:
    • сервис + БД;
    • сервис + очередь;
    • несколько модулей вместе.
  • Используют реальные инфраструктурные зависимости (часто в docker-compose).
  • Цель:
    • убедиться, что wiring, SQL, миграции, конфигурация и протоколы работают как ожидается.
  1. End-to-End (E2E)/API тесты
  • Проверяют систему "как пользователь" или "как внешний клиент":
    • HTTP-запрос → сервис → БД → ответ.
  • Используются для критичных сценариев (логин, создание документа, оплата и т.п.).
  1. Контрактные тесты (особенно в микросервисах)
  • Проверяют, что сервисы соблюдают API-контракты (формат, поля, коды ответов).
  • Важны при активных интеграциях.
  1. Property-based, нагрузочные, регрессионные и др.
  • Более продвинутые техники, полезны, но как следующий шаг.

Как проектировать код под тесты

Ключ: тестируемость — следствие правильной архитектуры.

  • Разделение слоёв:
    • транспорт (HTTP/gRPC),
    • бизнес-логика (service),
    • доступ к данным (repository).
  • Зависимости через интерфейсы:
    • сервис зависит от DocumentRepository, а не от конкретного PostgreSQL-кода.
  • Чистые функции там, где это возможно:
    • чем меньше скрытых зависимостей (глобальных синглтонов, времени, IO), тем проще тестировать.

Пример unit-теста на Go для бизнес-логики

Предположим, у нас есть сервис, который создаёт документ:

type Document struct {
ID int64
Title string
Status string
}

type DocumentRepository interface {
Save(ctx context.Context, doc *Document) error
}

type DocumentService struct {
repo DocumentRepository
}

func (s *DocumentService) Create(ctx context.Context, title string) (*Document, error) {
if strings.TrimSpace(title) == "" {
return nil, fmt.Errorf("empty title")
}

doc := &Document{
Title: title,
Status: "draft",
}

if err := s.repo.Save(ctx, doc); err != nil {
return nil, err
}

return doc, nil
}

Пишем unit-тест с "фейковым" репозиторием:

type fakeRepo struct {
saved []*Document
err error
}

func (r *fakeRepo) Save(_ context.Context, doc *Document) error {
if r.err != nil {
return r.err
}
doc.ID = int64(len(r.saved) + 1)
r.saved = append(r.saved, doc)
return nil
}

func TestDocumentService_Create_Success(t *testing.T) {
r := &fakeRepo{}
s := &DocumentService{repo: r}

doc, err := s.Create(context.Background(), "Contract #1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if doc.ID == 0 {
t.Fatalf("expected ID to be set")
}
if doc.Status != "draft" {
t.Fatalf("expected status draft, got %s", doc.Status)
}
}

func TestDocumentService_Create_EmptyTitle(t *testing.T) {
r := &fakeRepo{}
s := &DocumentService{repo: r}

_, err := s.Create(context.Background(), " ")
if err == nil {
t.Fatalf("expected error for empty title")
}
}

Что это демонстрирует:

  • бизнес-логика тестируется без реальной БД;
  • зависимости внедрены через интерфейс;
  • легко проверять граничные случаи.

Интеграционный тест с реальной БД (PostgreSQL)

Пример SQL-схемы:

CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Интеграционный тест (Go, упрощённо):

func TestPgDocumentRepository_Save_And_GetByID(t *testing.T) {
db, cleanup := setupTestDB(t) // поднимаем тестовую БД / транзакцию
defer cleanup()

repo := &PgDocumentRepository{db: db}

ctx := context.Background()
doc := &Document{
Title: "Test",
Status: "draft",
}

if err := repo.Save(ctx, doc); err != nil {
t.Fatalf("save error: %v", err)
}
if doc.ID == 0 {
t.Fatalf("expected ID assigned")
}

got, err := repo.GetByID(ctx, doc.ID)
if err != nil {
t.Fatalf("get error: %v", err)
}
if got == nil || got.Title != "Test" {
t.Fatalf("unexpected doc: %+v", got)
}
}

Здесь мы проверяем уже связку: SQL + репозиторий.

Подход к тестированию, который стоит демонстрировать

  • Не "иногда написал assert", а системный подход:
    • выделение слоёв;
    • интерфейсы для изоляции инфраструктуры;
    • осмысленные кейсы: успешный путь, невалидные данные, ошибки зависимостей.
  • Использование:
    • в Go: стандартный testing, table-driven tests, mocks/fakes;
    • в PHP: PHPUnit, Mockery/Prophecy, интеграция с CI.
  • Интеграция в pipeline:
    • тесты как обязательная часть CI;
    • падение тестов блокирует merge.

Кратко, как должен звучать зрелый ответ:

  • Да, использую тестирование в работе:
    • пишу unit-тесты для бизнес-логики;
    • использую интерфейсы для моков репозиториев и внешних клиентов;
    • для критичных частей — интеграционные тесты с реальной БД;
  • Понимаю ценность:
    • раннее выявление регрессий;
    • уверенность при рефакторинге;
    • документация поведения через тесты;
  • Проектирую код так, чтобы его можно было тестировать: без глобальных синглтонов, с явными зависимостями и чёткими контрактами.

Вопрос 32. Работал ли ты с NoSQL/Redis и какие типы данных там используются?

Таймкод: 00:26:33

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

Правильный ответ:

Под NoSQL обычно понимают классы систем, отличающихся от классических реляционных БД:

  • key-value хранилища (Redis, Memcached);
  • документо-ориентированные (MongoDB, CouchDB);
  • колоночные (Cassandra, HBase);
  • графовые (Neo4j и др.).

От кандидата ожидается понимание:

  • где NoSQL/Redis уместен,
  • какие структуры там есть,
  • как их использовать для типичных задач backend-а.

Ниже — сфокусируемся на Redis как типичном представителе in-memory key-value хранилища.

Ключевые типы данных Redis

Redis — это не просто "строка по ключу", а набор эффективных in-memory структур, доступных через команды. Основные типы:

  1. String
  • Базовый тип: бинарно-безопасная строка (любые данные до 512 МБ).
  • Используется как:
    • обычное значение;
    • счётчик;
    • флаг;
    • сериализованный JSON/protobuf.

Примеры команд:

  • SET key value
  • GET key
  • INCR/DECR key (атомарные операции по счётчику)

Практическое применение:

  • кэширование ответа:
    • SET user:123 '{"id":123,"name":"Alice"}' EX 60
  • счётчики запросов, лайков, rate limiting:
    • INCR page:views:post:42
  1. List
  • Упорядоченный список строк.
  • Операции:
    • LPUSH / RPUSH — добавить слева/справа;
    • LPOP / RPOP — извлечь слева/справа;
    • LRANGE — получить диапазон.

Использование:

  • простые очереди задач;
  • буферы логов/событий;
  • FIFO/LIFO структуры.

Пример:

  • очередь задач на отправку email:
    • LPUSH email_queue '{"to":"a@b.com","subj":"Hi"}'
    • воркер делает BRPOP email_queue.
  1. Set
  • Множество уникальных значений (без порядка).
  • Операции:
    • SADD, SREM, SISMEMBER;
    • SINTER, SUNION, SDIFF — операции над множествами.

Использование:

  • хранение уникальных элементов:
    • набор ролей, тегов, онлайн-пользователей;
  • быстрые проверки вхождения.

Пример:

  • онлайн-пользователи:
    • SADD online_users user123
    • SISMEMBER online_users user123
  1. Sorted Set (ZSet)
  • Множество элементов с числовым score, упорядоченных по этому score.
  • Операции:
    • ZADD — добавить с score;
    • ZRANGE / ZREVRANGE — получить по рангу;
    • ZRANGEBYSCORE — по диапазону score;
    • ZREM — удалить.

Использование:

  • рейтинги и лидерборды;
  • отложенные задачи (score = timestamp);
  • сортировка по весу/времени.

Примеры:

  • топ постов:

    • ZINCRBY post:rating 1 post:42
    • ZREVRANGE post:rating 0 9 — топ 10.
  • планирование задач:

    • ZADD tasks 1712230000 "job:123"
    • воркер периодически:
      • ZRANGEBYSCORE tasks 0 now — забрать задачи "созревшие" к текущему времени.
  1. Hash
  • Ассоциативный массив (key → (field → value)).
  • Операции:
    • HSET, HGET, HGETALL, HINCRBY.

Использование:

  • хранение объектоподобных структур:
    • профиль пользователя,
    • настройки.

Пример:

HSET user:123 name "Alice" email "a@b.com"
HGETALL user:123
  1. Bitmap / Bitfield
  • Хранение и операции над битами внутри строки.
  • Использование:
    • трекинг присутствия/активности по дням,
    • дешёвые boolean-флаги на большом диапазоне id.
  1. HyperLogLog
  • Структура для приблизительного подсчёта количества уникальных элементов с малым потреблением памяти.
  • Команды: PFADD, PFCOUNT.
  • Использование:
    • уникальные посетители, уникальные события.
  1. Streams
  • Логоподобная структура для событий.
  • Использование:
    • event sourcing,
    • очереди с потребителями/группами.
  1. Geo
  • Набор команд для хранения и поиска геокоординат (GEOADD, GEORADIUS).

Важно: "деревья" как отдельный тип в Redis нет; сложные структуры строятся поверх имеющихся типов (ZSet, Hash, Set и т.д.).

Где Redis и NoSQL реально применяются

Практичные сценарии:

  • Кэширование:
    • кэш HTTP-ответов, профилей, конфигураций;
    • снижение нагрузки на PostgreSQL.
  • Rate limiting:
    • INCR + EXPIRE по ключам:
      • rate:user:{id}:{minute}.
  • Сессии и токены:
    • хранение с TTL.
  • Очереди и отложенные задачи:
    • List, Streams, ZSet (по времени).
  • Лимиты/счётчики:
    • счётчики просмотров, кликов, лайков:
      • Redis как быстрый слой + периодический сброс в основную БД.
  • Множества и отношения:
    • кто онлайн, подписчики, фолловеры, быстрые set-операции.

Базовые принципы использования Redis

  • In-memory, очень быстрый:
    • но данные volatile, если не настроен persistence/replication;
  • Atomic operations:
    • INCR, HINCRBY, ZINCRBY, SETNX и др. — атомарны;
    • это важно для конкурентных сценариев (лайки, лимиты, локи).
  • TTL/EXPIRE:
    • удобно для кэшей и временных данных.

Пример небольшого кейса (Go + Redis): счётчик просмотров

// Инкремент просмотров поста
func (s *Service) IncrementPostView(ctx context.Context, postID int64) error {
key := fmt.Sprintf("post:%d:views", postID)
return s.redis.Incr(ctx, key).Err()
}

// Получить количество просмотров
func (s *Service) GetPostViews(ctx context.Context, postID int64) (int64, error) {
key := fmt.Sprintf("post:%d:views", postID)
return s.redis.Get(ctx, key).Int64()
}

С периодическим сбросом в PostgreSQL:

UPDATE posts
SET views = views + $delta
WHERE id = $post_id;

Кратко для собеседования:

  • Да, знаком с Redis и его моделью:
    • строки, списки, множества, отсортированные множества, хэши, стримы и др.
  • Понимаю, что Redis:
    • отлично подходит для кэшей, счетчиков, очередей, rate limiting;
    • даёт атомарные операции и высокую скорость;
    • требует осознанной работы с персистентностью и потерей данных.
  • В более широком контексте NoSQL:
    • выбираю key-value/документ/колоночное хранилище, когда нужна горизонтальная масштабируемость, гибкая схема или специфические паттерны доступа, а не ад-хок замена реляционной БД.

Вопрос 33. Знаком ли ты с поисковыми движками (Elasticsearch, Solr) и полнотекстовым поиском?

Таймкод: 00:27:18

Ответ собеседника: неполный. Упоминает, что слышал про Elasticsearch и немного работал с полнотекстовым поиском в PostgreSQL, но не демонстрирует системного понимания поисковых движков и принципов полнотекстового поиска.

Правильный ответ:

Полнотекстовый поиск — это отдельный класс задач, принципиально отличающийся от простых LIKE '%word%' по требованиям к качеству и производительности. Важно понимать:

  • какие задачи он решает;
  • чем специализированные поисковые движки отличаются от реляционных БД;
  • как использовать встроенный полнотекстовый поиск PostgreSQL;
  • когда выбирать Elasticsearch/Solr, а когда достаточно возможностей БД.

Ключевые концепции полнотекстового поиска

Основные задачи, которых нет/мало в обычном LIKE:

  • токенизация текста (разбиение на слова с учётом языка);
  • нормализация:
    • приведение к нижнему регистру;
    • стемминг/лемматизация (document → докум, документов → документ);
  • стоп-слова (и, или, но, the, a и т.п.);
  • поиск по нескольким полям и документам;
  • релевантность и рейтинг:
    • "лучшие совпадения выше";
  • поиск по фразам, близости слов, логическим выражениям;
  • масштабируемость по объёму данных и запросов.

Полнотекстовый поиск в PostgreSQL (встроенный механизм)

PostgreSQL предоставляет мощный полнотекстовый поиск "из коробки". Основные элементы:

  • tsvector — нормализованное представление текста;
  • tsquery — запрос (набор искомых терминов/операторов);
  • функции:
    • to_tsvector(language, text)
    • to_tsquery(language, query)
    • plainto_tsquery(language, text)
  • индексы:
    • GIN (основной вариант для быстрого FTS);
    • GiST (можно использовать, но GIN обычно быстрее при чтении).

Простой пример:

CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL
);

-- Индекс для полнотекстового поиска по title+body
CREATE INDEX idx_documents_fts
ON documents
USING GIN (to_tsvector('russian', coalesce(title,'') || ' ' || coalesce(body,'')));

Запрос поиска:

SELECT id, title,
ts_rank_cd(
to_tsvector('russian', coalesce(title,'') || ' ' || coalesce(body,'')),
plainto_tsquery('russian', 'договор аренды')
) AS rank
FROM documents
WHERE to_tsvector('russian', coalesce(title,'') || ' ' || coalesce(body,'')) @@
plainto_tsquery('russian', 'договор аренды')
ORDER BY rank DESC
LIMIT 20;

Что важно понимать:

  • используется языковой словарь ('russian', 'english', и т.д.) для стемминга;
  • оператор @@ — "соответствует ли документ запросу";
  • ts_rank/ts_rank_cd — вычисление релевантности;
  • GIN-индекс обеспечивает быстрый поиск по большим объёмам текста;
  • вычисление to_tsvector в WHERE должно совпадать с выражением в индексе, иначе индекс не используется:
    • хорошая практика: хранить tsvector в отдельной колонке и поддерживать триггером.

Пример с отдельной колонкой:

ALTER TABLE documents
ADD COLUMN fts tsvector;

CREATE INDEX idx_documents_fts ON documents USING GIN (fts);

CREATE FUNCTION documents_fts_trigger()
RETURNS trigger AS $$
BEGIN
NEW.fts :=
to_tsvector('russian',
coalesce(NEW.title,'') || ' ' || coalesce(NEW.body,'')
);
RETURN NEW;
END
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_documents_fts
BEFORE INSERT OR UPDATE ON documents
FOR EACH ROW EXECUTE FUNCTION documents_fts_trigger();

Тогда запрос:

SELECT id, title
FROM documents
WHERE fts @@ plainto_tsquery('russian', 'договор аренды')
ORDER BY ts_rank_cd(fts, plainto_tsquery('russian', 'договор аренды')) DESC
LIMIT 20;

Когда этого достаточно:

  • внутренняя система документооборота;
  • поиск по ограниченному объёму (десятки/сотни тысяч, миллионы строк — ок при нормальной архитектуре);
  • когда не нужны сложные сценарии шардирования и сложный scoring.

Elasticsearch / Solr: в чём суть специализированных движков

И Elasticsearch, и Solr — это поисковые движки поверх inverted index (обратного индекса):

  • данные ориентированы на поиск по тексту;
  • масштабирование горизонтальное (шарды, реплики);
  • поддержка сложных запросов и scoring "из коробки";
  • ориентированы на JSON/документы (Elasticsearch) или XML/JSON (Solr).

Ключевые особенности Elasticsearch (актуально для практики):

  • Хранит документы в индексах и шардирует их по cluster nodes.
  • Поддерживает:
    • анализаторы (analyzers) для языков, стемминга, синонимов;
    • сложные запросы:
      • match, multi_match;
      • bool-запросы (must/should/must_not);
      • range, prefix, wildcard и т.д.;
    • гибкую настройку scoring;
    • агрегаты (aggregation) для аналитики поверх поискового индекса.
  • Хорош для:
    • полнотекстового поиска с релевантностью;
    • логов и метрик (ELK-стек);
    • сложной аналитики по тексту.

Solr:

  • Более старый и зрелый (поверх Lucene);
  • Схож по идее: полнотекст, inverted index, HTTP API;
  • Чаще используется в enterprise-организациях, библиотеках, старых проектах.

Когда выбирать Elasticsearch / Solr вместо одного PostgreSQL FTS

  • Требуется:
    • масштабировать поиск на десятки/сотни миллионов документов, много запросов;
    • "google-подобный" поиск с релевантностью, подсветкой, синонимами, fuzziness;
    • сложные агрегаты по тексту и полям;
    • отдельный поисковый кластер, разгружающий боевую БД.
  • Нужна:
    • высокая доступность и распределённость;
    • быстрый near real-time поиск по потокам данных (логи, события).

Типичная архитектура:

  • Данные "истины" — в PostgreSQL (или другой OLTP-БД).
  • Изменения (insert/update/delete) публикуются в Kafka/очередь.
  • Оттуда индексируются в Elasticsearch.
  • Чтение сложных поисковых запросов — из Elasticsearch.
  • Критичные транзакционные операции — из PostgreSQL.

Важные инженерные моменты:

  • согласованность:
    • ES обычно eventual consistent относительно основной БД;
    • нужно уметь жить с небольшими задержками в индексации.
  • стратегия обновления:
    • полная переиндексация или инкрементальная;
    • выбор id документа как _id в ES.

Простой пример интеграции (Go → Elasticsearch, концептуально):

type Document struct {
ID int64 `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
}

func indexDocumentToES(doc Document) error {
// Псевдо: используем официальный клиент ES
_, err := esClient.Index(
"documents",
esutil.NewJSONReader(doc),
esClient.Index.WithDocumentID(strconv.FormatInt(doc.ID, 10)),
)
return err
}

Краткое резюме, которое стоило бы дать на интервью:

  • Да, понимаю разницу:
    • PostgreSQL FTS:
      • встроенный, надёжный, транзакционный;
      • подходит для многих бизнес-систем;
      • использует tsvector, tsquery, GIN-индексы.
    • Elasticsearch/Solr:
      • специализированные поисковые движки;
      • inverted index, масштабирование, гибкий scoring, анализаторы.
  • На практике:
    • для внутреннего полнотекстового поиска по документам, логам, комментариям могу использовать PostgreSQL FTS;
    • при росте требований по объёму, релевантности и нагрузке — выношу поиск в отдельный сервис на Elasticsearch, синхронизируя данные из основной БД.
  • Понимаю базовые принципы:
    • токенизация, стемминг, стоп-слова;
    • индексация текстов и поиск по индексу, а не линейный LIKE;
    • eventual consistency при связке "OLTP-БД + поисковый кластер".

Вопрос 34. Какое направление разработки тебе ближе и как ты относишься к фронтенду?

Таймкод: 00:27:43

Ответ собеседника: правильный. Говорит, что ближе к backend, местами fullstack; к фронтенду относится нейтрально, выполнял простые задачи по правке стилей и элементов.

Правильный ответ:

На такой вопрос важно ответить честно и осознанно, показывая фокус и при этом нормальное отношение к соседним областям.

Оптимальный ответ может звучать так:

  • Основной фокус — серверная разработка:

    • проектирование и реализация API (HTTP/REST/gRPC);
    • работа с базами данных (PostgreSQL, транзакции, индексы, оптимизация запросов);
    • интеграция с внешними сервисами (SOAP/REST, очереди, брокеры сообщений);
    • обеспечение надёжности, производительности и наблюдаемости сервисов (логирование, метрики, трейсинг);
    • работа с очередями, кэшем (Redis), полнотекстовым поиском (PostgreSQL FTS, при необходимости Elasticsearch).
  • Отношение к фронтенду:

    • без фанатизма, но с уважением к сложности:
      • понимаю базовые вещи: HTML/CSS, немного JS/TypeScript, верстка, простые правки.
      • могу при необходимости:
        • поправить верстку,
        • добавить простой интерактив,
        • подебажить запросы к API в браузере.
    • при этом осознанно выбираю специализацию на backend:
      • нравится работать с данными, протоколами, архитектурой;
      • интересует качество API, консистентность, масштабируемость, транзакционность;
      • комфортно отвечать за серверную часть end-to-end: от схемы БД и доменной модели до деплоя сервиса.
  • Взаимодействие с фронтендом:

    • считаю важным:
      • проектировать API так, чтобы фронтенду было удобно и предсказуемо;
      • обсуждать контракты, форматы данных, коды ответов;
      • обеспечивать стабильность и обратную совместимость;
    • нормально отношусь к задачам на стыке:
      • настройка CORS,
      • обработка ошибок на фронте,
      • версия API и миграции.

Такой ответ показывает:

  • чёткий фокус на backend (что важно для позиции Go/серверной разработки);
  • понимание ценности фронтенда без противопоставления;
  • готовность взаимодействовать и помогать на стыке, но без распыления фокуса.

Вопрос 35. Готов ли ты работать с Symfony, WordPress и только фронтенд-задачами при необходимости?

Таймкод: 00:29:28

Ответ собеседника: правильный. Говорит, что не привязан к конкретному фреймворку, хотел бы работать с Symfony; готов 1–2 месяца заниматься WordPress и до месяца — только фронтендом, без негативного отношения.

Правильный ответ:

На такой вопрос важно показать:

  • гибкость и профессиональное отношение к стеку;
  • понимание приоритетов (куда хочешь развиваться);
  • готовность закрывать необходимые задачи команды без "религиозных" споров по технологиям.

Сильный ответ может выглядеть так:

В приоритете для меня — развитие в серверной разработке: проектирование API, работа с БД, высоконагруженные и интеграционные задачи, переход и углубление в Go и современные backend-подходы. При этом к выбору фреймворков и связанных технологий отношусь прагматично.

  • Symfony:

    • комфортно работаю с ним и считаю хорошим фреймворком для структурированных backend-приложений:
      • DI-контейнер, маршрутизация, middleware, события;
      • чёткое разделение слоёв, удобная интеграция с БД, кэшами, очередями.
    • Готов развивать и поддерживать проекты на Symfony: API, сервисы, админки, интеграции.
  • WordPress:

    • Понимаю, что во многих командах и проектах он используется как быстрый инструмент:
      • лендинги, контентные проекты, корпоративные сайты;
      • плагины, темы, интеграция с существующими сервисами.
    • Готов в разумных пределах (например, 1–2 месяца или в рамках задач спринта) поддерживать и развивать WordPress-часть:
      • писать/дорабатывать плагины,
      • оптимизировать работу с БД,
      • решать интеграционные задачи.
    • При этом ожидаю, что стратегически мой фокус будет смещаться в сторону более "инженерных" backend-задач.
  • Фронтенд:

    • Нормально отношусь к фронтенду и базовым задачам:
      • правки верстки, стилей, форм;
      • интеграция с API, отладка запросов;
      • мелкие клиентские фичи.
    • Готов временно (например, до месяца или по необходимости спринта) фокусироваться на фронтенде, если это нужно команде.
    • В долгую предпочитаю оставаться на стороне серверной архитектуры, данных и интеграций.

Важный момент, который стоит явно донести:

  • Я готов помогать команде там, где нужен: Symfony, WordPress, фронтенд-задачи.
  • Но основной вектор развития вижу в сторону backend-разработки, микросервисов, работы с БД, очередями, инфраструктурой и Go.
  • Такой баланс позволяет:
    • закрывать текущие потребности бизнеса,
    • при этом развивать архитектурно сильный стек и не выгорать на задачах, которые полностью не совпадают с основной специализацией.

Вопрос 36. Как ты оцениваешь свой английский и можешь ли устно рассказать о SQL на английском?

Таймкод: 00:30:04

Ответ собеседника: неполный. Оценивает английский как средний по тестам, но слабый разговорный; при попытке объяснить JOIN-ы на английском допускает ошибки, общая идея частично понятна, но неуверенна.

Правильный ответ:

Здесь важно показать честную самооценку и способность использовать английский в рабочем контексте, особенно для технической коммуникации: обсуждение архитектуры, чтение документации, участие во встречах, объяснение базовых концепций (например, SQL).

Хороший ответ должен содержать:

  • реалистичную оценку уровня;
  • понимание, что технический английский обязателен;
  • демонстрацию умения объяснить ключевые вещи на английском.

Пример сильного, прикладного ответа:

  • Оценка уровня:

    • Понимаю техническую документацию, RFC, статьи, исходники без существенных проблем.
    • Могу читать и писать технические сообщения: комментарии в коде, pull requests, issue, архитектурные заметки.
    • Могу участвовать в созвонах на английском: объяснить архитектуру, задать уточняющие вопросы, обсудить проблемы, хотя иногда подбираю формулировки и могу переспросить.
    • Уровень примерно между B1 и B2, ближе к рабочему техническому: не идеальный разговорный, но достаточный для продуктивной инженерной коммуникации.
  • Конкретный пример объяснения на английском (про SQL и JOIN), который демонстрирует практический рабочий уровень:

    "In relational databases we usually split data into multiple normalized tables and then combine them using JOINs.

    The most common types are:

    • INNER JOIN: returns only rows that have matching keys in both tables. For example, if we join 'documents' with 'users' on 'user_id', we only get documents that have an existing author.
    • LEFT JOIN: returns all rows from the left table and matching rows from the right table if they exist. If there is no match, columns from the right table are NULL. This is useful when we want all documents, even if the author record is missing or was deleted.
    • RIGHT JOIN and FULL OUTER JOIN are similar concepts, but in practice I use INNER and LEFT JOIN most frequently.

    In terms of performance, it's important to:

    • have proper indexes on join keys;
    • avoid unnecessary joins;
    • and check execution plans with EXPLAIN to ensure we are not doing full table scans where we don't need them."
  • Такой ответ показывает:

    • понимание SQL-концепций;
    • умение выразить их на английском понятно для международной команды;
    • готовность использовать английский как рабочий инструмент, а не просто "по тесту".

Если требуется усилить:

  • Готов продолжать улучшать разговорный английский:
    • через ежедневное чтение технической документации и статей;
    • участие в созвонах;
    • практику устных объяснений сложных технических тем.
  • Английский не является блокером для участия в англоязычной команде, особенно в техническом контексте.

Вопрос 37. Что означает ошибка 500 и какие шаги предпринять для выяснения причины?

Таймкод: 00:32:05

Ответ собеседника: неполный. Верно указывает, что 500-я ошибка связана с проблемой на стороне сервера, упоминает таймауты и технические работы, но не описывает системный подход к диагностике: логи, трассировка, метрики, разбор стека, влияние инфраструктуры.

Правильный ответ:

Ошибка 500 (HTTP 500 Internal Server Error) означает:

  • сервер принял запрос;
  • при обработке произошла непредвиденная ошибка;
  • сервер не смог корректно завершить выполнение и вернуть более специфичный код.

Это "общий" индикатор сбоя на стороне сервиса: баги в коде, исключения, ошибки интеграций, проблемы с БД, конфигурацией, инфраструктурой. В зрелых системах задача — минимизировать "голые" 500 и максимально возвращать более точные коды (4xx/422/409/503 и т.п.), а 500 оставлять для действительно неожиданных ситуаций.

Системный подход к диагностике 500

Ключ: не гадать "на глаз", а идти по понятному чек-листу.

  1. Логи приложения

Первый шаг — смотреть логи сервера/сервиса вокруг времени возникновения ошибки:

  • error-логи:
    • stack trace исключений;
    • сообщения об ошибках БД (connection refused, timeout, constraint violation);
    • паники, null pointer, out of memory, неправильно распарсенный JSON и т.п.
  • access-логи:
    • метод, URL, статус-коды;
    • время ответа;
    • корреляция по request-id/trace-id.

Хорошая практика:

  • для каждого запроса генерировать X-Request-ID/trace-id;
  • логировать его и возвращать клиенту;
  • по нему связывать access-лог и error-лог.
  1. Метрики и алертинг

Проверяем:

  • резкий рост доли 5xx;
  • latency (p95/p99);
  • нагрузку на CPU/RAM;
  • пул соединений к БД (exhausted?);
  • количество открытых файлов/соединений.

Это помогает понять:

  • локальная ли проблема (один endpoint, один инстанс);
  • или системная (инфраструктура, база, внешние сервисы).
  1. Проверка зависимостей

500 очень часто — следствие проблем с внешними компонентами:

  • БД:
    • недоступна,
    • истёк timeout,
    • deadlock,
    • ошибки миграций,
    • нарушение ограничений (иногда следует маппить на 4xx, но если не обработано — падает в 500).
  • Redis/кэш:
    • connection refused, timeout.
  • Очереди, сторонние API (платежи, почта, S3):
    • ошибки подключения;
    • неверные креды;
    • изменение контракта.

Шаги:

  • проверить connectivity (ping, telnet, health checks);
  • сопоставить время 500 с логами и метриками зависимостей.
  1. Воспроизведение

Если 500 воспроизводим:

  • отправить тот же запрос (url, body, headers) в тестовой среде;
  • локально запустить сервис в режиме debug:
    • посмотреть stack trace;
    • выяснить, на каких данных и в какой ветке кода всё ломается.
  1. Маппинг ошибок и обработка исключений

В зрелом сервисе 500 часто означает:

  • незахваченное исключение;
  • отсутствие нормальной обработки предсказуемой ошибки.

Например:

  • нарушение уникального индекса (duplicate key):
    • это не "Internal Server Error", а 409 Conflict или 422.
  • неверные входные данные:
    • 400/422, а не паника.
  • недоступность внешнего сервиса:
    • чаще 502/503 (в зависимости от контекста), с ретраями и деградацией.

Правильные шаги:

  • централизованный error handling middleware:
    • логирует внутренние ошибки;
    • возвращает стандартизованный ответ (message, code, correlation id);
  • явное преобразование технических ошибок в корректные HTTP-коды там, где семантика известна;
  • неожиданные ошибки оставлять 500, но с хорошими логами.

Пример на Go: централизованный обработчик ошибок

type AppHandler func(w http.ResponseWriter, r *http.Request) error

func withErrorHandling(h AppHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := h(w, r)
if err == nil {
return
}

// Пример маппинга
var ve *ValidationError
switch {
case errors.As(err, &ve):
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(map[string]any{
"error": "validation_error",
"fields": ve.Fields,
})
case errors.Is(err, ErrNotFound):
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]any{
"error": "not_found",
})
default:
// Логируем внутреннюю ошибку с trace-id
log.Printf("internal error: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]any{
"error": "internal_error",
"request_id": getRequestID(r),
})
}
}
}

Такой подход:

  • минимизирует голые 500;
  • делает диагностику проще (в логах есть всё нужное);
  • клиенту возвращает осмысленную информацию.
  1. Инфраструктурные причины

Иногда 500 генерирует не приложение, а:

  • reverse proxy / API Gateway (Nginx/Envoy/Traefik):
    • upstream timeout;
    • bad gateway (502) → иногда маскируется;
  • container platform/Kubernetes:
    • pod не отвечает, рестарты;
  • мисконфиг:
    • неправильные переменные окружения;
    • отсутствие нужных секретов/файлов.

Проверяем:

  • логи балансировщика/gateway;
  • health-check endpoints;
  • состояние pod-ов/инстансов.

Итоговый чек-лист действий при 500

  • Зафиксировать параметры запроса:
    • URL, метод, body (без чувствительных данных), headers, время.
  • Найти связанный лог по request-id.
  • Посмотреть:
    • stack trace;
    • ошибки БД/Redis/HTTP-клиентов;
    • метрики.
  • Определить:
    • это бага в логике? некорректная обработка известных ошибок? проблема инфраструктуры?
  • Исправить:
    • добавить обработку ошибки и правильный HTTP-код;
    • починить конфигурацию/зависимость;
    • покрыть тестом кейс, который привёл к 500.

Кратко для собеседования:

  • 500 — это общий индикатор внутренней ошибки сервера.
  • Для диагностики:
    • смотрю логи, метрики, трассировки, состояние зависимостей;
    • воспроизвожу запрос;
    • убеждаюсь, что предсказуемые ошибки маппятся на корректные HTTP-коды.
  • Стремлюсь к тому, чтобы:
    • 500 означал действительно неожиданный сбой,
    • а не отсутствие нормальной обработки типовых ситуаций.

Вопрос 38. Как определить и проанализировать медленные запросы к базе данных?

Таймкод: 00:33:04

Ответ собеседника: неполный. Указывает на замер времени выполнения и использование EXPLAIN, предлагает уменьшать количество колонок и пересматривать подзапросы и JOIN-ы. Направление верное, но не хватает системного подхода: профилирование, slow query логи, метрики, анализ планов, влияние индексов и структуры данных.

Правильный ответ:

Для работы с производительностью SQL важно не “угадывать”, а применять системный, воспроизводимый процесс:

  1. найти медленные запросы;
  2. понять, почему они медленные (план, индексы, объёмы данных);
  3. оптимизировать (структура запроса, индексы, схема данных, кэширование).

Ниже — практический подход, ориентированный на PostgreSQL (аналогично применимо к MySQL и другим).

Поиск медленных запросов

Основные источники:

  1. Лог медленных запросов (slow query log)

В PostgreSQL:

  • включаем логирование медленных запросов:
    • log_min_duration_statement — порог в миллисекундах.
      • Например, log_min_duration_statement = 200ms или 500ms.
  • все запросы дольше порога попадут в логи.

Это:

  • даёт реальные данные по продакшену;
  • позволяет увидеть паттерны: тяжёлые JOIN-ы, seq scan-ы по большим таблицам, отсутствующие индексы.

В MySQL:

  • slow_query_log, long_query_time.
  1. Application-level метрики

На уровне кода:

  • измеряем время выполнения запросов:
    • логируем запросы > X ms;
    • сохраняем метрики в Prometheus / OpenTelemetry.
  • анализируем:
    • percentiles (p95, p99);
    • по endpoint-ам, по типам запросов.
  1. Профилирование конкретного кейса

Когда есть конкретный “подозреваемый” запрос:

  • воспроизводим его отдельно:
    • с реальными параметрами;
    • на боевых или приближенных данных;
  • смотрим план выполнения.

Анализ плана выполнения (EXPLAIN / EXPLAIN ANALYZE)

Ключевой инструмент — EXPLAIN.

В PostgreSQL:

  • EXPLAIN — показывает план (как БД собирается выполнять запрос).
  • EXPLAIN ANALYZE — выполняет запрос и показывает фактические времена и размеры.

Пример:

EXPLAIN ANALYZE
SELECT d.id, d.title, u.name
FROM documents d
JOIN users u ON u.id = d.user_id
WHERE d.created_at >= now() - interval '7 days'
ORDER BY d.created_at DESC
LIMIT 50;

Смотрим:

  • используется ли индекс по documents.created_at, documents.user_id, users.id;
  • есть ли Seq Scan по большой таблице там, где ожидали Index Scan;
  • не происходит ли "Nested Loop" по огромным объёмам;
  • оценка vs реальные строки:
    • если планировщик сильно ошибается, возможно устарела статистика.

Типичные причины медленных запросов

  1. Отсутствующие или неверные индексы

Симптомы:

  • последовательное сканирование большой таблицы (Seq Scan) там, где фильтр по селективному полю;
  • медленные JOIN-ы по неиндексированным ключам.

Решения:

  • добавить индексы по колонкам в WHERE/JOIN/ORDER BY:
    CREATE INDEX idx_documents_user_id ON documents (user_id);
    CREATE INDEX idx_documents_created_at ON documents (created_at);
  • для составных условий — составные индексы:
    CREATE INDEX idx_documents_user_created
    ON documents (user_id, created_at);
  1. Слишком тяжёлые выборки

Симптомы:

  • SELECT * по широкой таблице;
  • выкачивание тысяч+ строк там, где фронтенду нужно 20.

Решения:

  • выбирать только нужные поля;
  • делать пагинацию:
    • LIMIT (и лучше keyset-пагинацию для больших объёмов);
  • избегать лишних JOIN-ов и подзапросов, если данные не используются.
  1. Плохие JOIN-ы и подзапросы

Проблемы:

  • JOIN по неиндексированным полям;
  • "картезианские" соединения из-за отсутствия корректных условий;
  • подзапросы, которые не используют индексы или не коррелируют оптимально.

Решения:

  • убедиться, что по ключам JOIN-а есть индексы;
  • проверять условия соединения;
  • иногда переписать подзапрос как JOIN или наоборот, чтобы дать оптимизатору лучший план.
  1. Проблемы с сортировкой и GROUP BY

Симптомы:

  • сортировка (ORDER BY) или агрегация (GROUP BY) по большим наборам без индекса;
  • использование DISTINCT для "лечения" неверных JOIN-ов.

Решения:

  • индекс под сортировку:
    CREATE INDEX idx_documents_created_at ON documents (created_at DESC);
  • пересмотреть логику:
    • правильный JOIN вместо "JOIN + DISTINCT";
    • агрегация по индексируемым полям.
  1. Неактуальная статистика

Оптимизатор полагается на статистику таблиц.

Если она устарела:

  • выбираются невыгодные планы;
  • возможны Seq Scan вместо Index Scan.

Решения:

  • ANALYZE / автообновление статистики:
    ANALYZE documents;
  • следить за autovacuum/autonalyze.

Системный алгоритм действий

  1. Найти медленные запросы:
  • включить slow query log;
  • собрать метрики по latency;
  • выделить топ проблемных.
  1. Для каждого:
  • зафиксировать конкретный SQL и параметры;
  • выполнить EXPLAIN ANALYZE;
  • идентифицировать:
    • где тратится время (scan/sort/hash join/nested loop);
    • какие индексы (не) используются.
  1. Оптимизировать:
  • добавить/поправить индексы;
  • упростить запрос, убрать лишние JOIN-ы;
  • заменить SELECT * на явные поля;
  • пересмотреть пагинацию (OFFSET → keyset);
  • при необходимости нормализовать/денормализовать данные.
  1. Проверить результат:
  • повторить EXPLAIN ANALYZE;
  • убедиться в снижении времени и разумности плана;
  • следить по метрикам в продакшене.

Пример: оптимизация запроса на практике

Было (медленно, seq scan):

SELECT *
FROM documents
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 20;

План:

  • Seq Scan по documents (миллионы строк), потом сортировка.

Решение:

  • добавить индекс:
CREATE INDEX idx_documents_user_created
ON documents (user_id, created_at DESC);

Стало:

  • Index Scan по user_id с уже отсортированными по created_at строками;
  • берём первые 20 — быстро и без полной сортировки.

Кратко, как это стоит формулировать на собеседовании:

  • Для поиска медленных запросов:
    • использую slow query log, метрики на уровне приложения, профилирование;
  • Для анализа:
    • применяю EXPLAIN/EXPLAIN ANALYZE;
    • смотрю планы, индексы, типы сканов, объёмы данных;
  • Для оптимизации:
    • настраиваю индексы под реальные запросы;
    • упрощаю запросы, убираю лишние JOIN-ы и SELECT *;
    • корректно реализую пагинацию;
    • слежу за актуальностью статистики;
  • Всегда опираюсь на измерения и планы, а не интуицию.

Вопрос 39. Есть ли у тебя опыт работы с CSS-фреймворками?

Таймкод: 00:34:07

Ответ собеседника: правильный. Прямо говорит, что с CSS-фреймворками не работал.

Правильный ответ:

Здесь важна честность и умение адекватно оценивать свои навыки, без попыток "доиграть" опыт, которого нет.

Оптимальный ответ может быть таким:

  • Прямого практического опыта с крупными CSS-фреймворками (Bootstrap, Tailwind, Foundation, Bulma и др.) у меня нет.
  • Однако базовые вещи во фронтенде понимаю:
    • стандартный CSS, flex/grid, адаптивная верстка на уровне поддержки и простых правок;
    • структура HTML, классы, работа с devtools в браузере;
    • могу аккуратно доработать верстку, исправить отступы, цвета, шрифты, состояния элементов.
  • При необходимости быстро осваиваю конкретный фреймворк:
    • CSS-фреймворки — это, по сути, набор готовых классов/компонентов и правил, а не принципиально другая технология;
    • документация Bootstrap/Tailwind достаточно проста, чтобы за 1–3 дня начать уверенно использовать основные компоненты (layout, сетка, формы, кнопки, модалки).
  • К фронтенд-части отношусь прагматично:
    • если для задачи команды нужно подключить или поддержать UI на готовом CSS-фреймворке — готов это сделать;
    • при этом основной фокус остаётся на backend и API, но базовый UI-слой поддержать могу без проблем.

Такой ответ:

  • не завышает компетенции;
  • показывает, что отсутствие опыта с конкретным CSS-фреймворком не является блокером;
  • демонстрирует способность быстро адаптироваться к сопутствующим технологиям.

Вопрос 40. Есть ли у тебя опыт прямого общения с заказчиками?

Таймкод: 00:34:21

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

Правильный ответ:

Здесь важна честность и понимание, как опыт, близкий к работе с заказчиком, помогает в инженерной роли.

Сильный ответ может выглядеть так:

  • Прямого постоянного общения с заказчиками в формате "аккаунт-менеджмента" или ведения проектов у меня немного.
  • Однако:
    • работал в техподдержке/по линии поддержки пользователей:
      • принимал обращения,
      • уточнял детали проблем,
      • переводил пользовательские формулировки в технические задачи,
      • объяснял решения простым языком.
    • участвовал в обсуждении требований через тимлида/аналитика:
      • задавал уточняющие вопросы;
      • помогал формализовать сценарии и ограничения;
      • предлагал технически обоснованные варианты реализации.

Этот опыт полезен, потому что:

  • помогает правильно задавать вопросы и не бояться уточнять неполные требования;
  • учит отделять реальную бизнес-проблему от "формулировки в тикете";
  • позволяет объяснять технические ограничения и решения доступным языком;
  • улучшает качество API и внутренних инструментов — меньше "разрывов" между ожиданиями и фактом.

Готовность на будущее:

  • Готов к прямому общению с заказчиками или представителями бизнеса:
    • в рамках уточнения требований;
    • демонстрации функционала;
    • обсуждения компромиссов между сроками, сложностью и архитектурой.
  • При этом считаю важным:
    • сохранять прозрачность (не обещать невыполнимое);
    • фиксировать договорённости в понятном виде (задачи, спецификации);
    • переводить бизнес-язык в технические решения и обратно.

Такой ответ показывает зрелое отношение: нет выдуманного опыта, но есть релевантные навыки и готовность к ответственному общению с заказчиком.

Вопрос 41. Есть ли у тебя опыт работы с облачной инфраструктурой и серверами?

Таймкод: 00:34:48

Ответ собеседника: неполный. Отвечает неуверенно, путает инструменты, упоминает Jira в контексте облаков, не демонстрирует реального опыта с облачной инфраструктурой и управлением серверами.

Правильный ответ:

На такой вопрос важно:

  • честно обозначить текущий уровень;
  • показать понимание базовых концепций: деплой, окружения, контейнеры, мониторинг;
  • продемонстрировать готовность и способность работать с облаком и серверной инфраструктурой, особенно в контексте backend/Go.

Если реального продвинутого опыта мало, хороший ответ строится так: минимум воды, максимум практических понятий.

Ключевые области, которые ожидают:

  1. Базовое понимание серверов и окружений

Даже без специфичного облака (AWS/GCP/Azure) необходимо уверенно понимать:

  • окружения: dev/stage/prod;
  • деплой приложения:
    • сборка артефакта (binary/container),
    • конфигурации через переменные окружения,
    • миграции БД;
  • управление зависимостями:
    • подключение к PostgreSQL/Redis/очередям по DSN/URL;
    • секреты не в коде (env/secret manager).

Минимальный набор, который должен быть под рукой:

  • умение зайти на сервер (SSH), посмотреть логи, состояние сервиса:
    • journalctl, systemctl status, docker logs;
  • понимание reverse proxy:
    • Nginx/Traefik как фронт к backend-сервису;
  • базовая диагностика:
    • ping, curl, netstat/ss, проверка портов.
  1. Контейнеры и Docker

В современной инфраструктуре это почти must-have.

Ожидается понимание:

  • как упаковать сервис в Docker-образ:
    • минимальный Dockerfile;
    • multi-stage build для Go;
  • как прокинуть конфиг/переменные окружения;
  • как локально поднять связку сервис + PostgreSQL + Redis через docker-compose.

Пример Dockerfile для Go-сервиса:

FROM golang:1.22-alpine AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY ../blog-draft .
RUN go build -o app ./cmd/app

FROM alpine:3.19
WORKDIR /app
COPY --from=build /app/app .
EXPOSE 8080
ENTRYPOINT ["./app"]
  1. Облачные провайдеры (концептуально)

Даже если опыт ограничен, важно понимать, что в типичном облаке используются:

  • Compute:
    • виртуальные машины (EC2/GCE/Droplets);
    • контейнерные платформы (ECS/EKS/GKE/Kubernetes);
  • Storage и БД:
    • управляемые PostgreSQL/MySQL (RDS, Cloud SQL);
    • object storage (S3-совместимый) для файлов;
  • Networking:
    • балансировщики нагрузки (ALB/NLB);
    • security groups/firewall;
  • Observability:
    • логи, метрики, алерты (CloudWatch, Stackdriver, Prometheus+Grafana);
  • Secrets:
    • Secret Manager / Parameter Store / Vault.

Можно честно сказать:

  • Если не работал плотно с конкретным облаком:
    • "С конкретным провайдером (AWS/GCP/Azure) в продакшене пока большого опыта нет, но знаком с базовыми концепциями: виртуальные машины, контейнеры, load balancer, managed databases, object storage. Умею разворачивать и отлаживать сервисы в Linux-среде, работать с Docker, настраивать переменные окружения, подключение к БД и логирование. Осознанно отношусь к вопросам безопасности (не хранить секреты в коде, минимальные права доступа)."
  1. CI/CD и деплой

Сильным плюсом будет понимание:

  • как строится pipeline:
    • запуск тестов;
    • сборка артефакта/образа;
    • деплой на сервер/в кластер;
  • использование:
    • GitLab CI, GitHub Actions, Jenkins или аналогов.

Даже кратко:

  • "Привык, что деплой автоматизирован:
    • каждый push/merge запускает тесты;
    • при успехе собирается Docker-образ и выкатывается на staging/production.
    • В случае Go/сервисов понимаю, как встроить миграции БД перед запуском новой версии."
  1. Как правильно это подать, если продвинутого опыта нет

Хороший, честный и при этом профессиональный ответ:

  • Прямого глубокого опыта управления облачной инфраструктурой (типа самостоятельной настройки AWS/GCP кластера с нуля) пока нет.
  • При этом:
    • уверенно чувствую себя в Linux-среде;
    • умею собирать и запускать приложения в Docker;
    • понимаю базовые компоненты продакшн-инфраструктуры: балансировщик → приложение → БД → кэш → очередь;
    • знаю, как работать с логами и метриками для диагностики проблем.
  • Готов и мотивирован углубиться:
    • в конкретный стек, который используется в вашей компании (AWS/GCP/Azure/Kubernetes);
    • в практики CI/CD, наблюдаемости, безопасного деплоя.

Такой ответ:

  • не завышает компетенции;
  • показывает фундамент, на который можно быстро "навесить" конкретный облачный стек;
  • демонстрирует инженерное мышление и готовность брать ответственность за сервис от кода до продакшна.

Вопрос 42. Как у тебя с Docker и участвовал ли ты в деплое приложений?

Таймкод: 00:35:07

Ответ собеседника: неполный. Говорит, что умеет написать Dockerfile и запускать контейнеры; упоминает деплой на Render со стажировки, но без деталей и системного понимания процесса.

Правильный ответ:

Ожидается не только знание команды docker run, а понимание роли Docker в современном backend-стеке, типового пайплайна деплоя и практических нюансов.

Ключевые моменты, которые стоит уметь объяснить:

  1. Зачем нужен Docker

Docker решает несколько задач:

  • единое, воспроизводимое окружение:
    • "работает у меня" превращается в "работает одинаково в dev/stage/prod";
  • изоляция:
    • зависимости приложения не конфликтуют между проектами;
  • удобный деплой:
    • приложение + зависимости упакованы в образ;
    • одинаково запускается локально, на сервере, в Kubernetes, Render, ECS и т.п.

Для backend-сервисов (Go, PHP, Symfony, etc.) это стандарт.

  1. Базовое владение Docker

Важно уверенно уметь:

  • писать минимальный, адекватный Dockerfile под приложение;
  • собирать образ:
    • docker build -t myapp:latest .
  • запускать контейнер:
    • docker run -p 8080:8080 --env-file .env myapp:latest
  • использовать docker-compose для локальной разработки:
    • сервис + PostgreSQL + Redis + вспомогательные компоненты.
  1. Пример Dockerfile для Go-сервиса (best practice)

Multi-stage build для минимального образа:

# Этап сборки
FROM golang:1.22-alpine AS build

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app ./cmd/app

# Этап рантайма
FROM gcr.io/distroless/base-debian12

WORKDIR /app
COPY --from=build /app/app .

EXPOSE 8080
USER nonroot:nonroot

ENTRYPOINT ["/app/app"]

Ключевые моменты:

  • разделение build/runtime:
    • final-образ маленький, без лишних тулов;
  • CGO_ENABLED=0 + distroless:
    • минимальная поверхность для атак;
  • USER nonroot:
    • безопасный запуск без root.
  1. Пример Dockerfile для PHP/Symfony (кратко)
FROM php:8.2-fpm-alpine

WORKDIR /var/www/html

RUN docker-php-ext-install pdo pdo_pgsql

COPY . .

# composer install в отдельном шаге/образе
# настройка прав, opcache и т.п.

EXPOSE 9000
CMD ["php-fpm"]

Часто в связке с Nginx-контейнером.

  1. Docker Compose для локальной разработки

Пример для Go-сервиса + PostgreSQL:

version: "3.9"
services:
api:
build: .
ports:
- "8080:8080"
environment:
- DB_DSN=postgres://user:pass@db:5432/app?sslmode=disable
depends_on:
- db

db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
ports:
- "5432:5432"

Это демонстрирует понимание:

  • как сервис зависит от БД;
  • как задаются DSN через env;
  • как поднимать окружение командой docker-compose up.
  1. Участие в деплое приложений

Даже если деплойом занимались DevOps/SRE, важно:

  • понимать типовую схему:

    • CI:
      • запуск тестов;
      • сборка Docker-образа;
      • публикация в registry (Docker Hub, ECR, GCR, GitLab Registry).
    • CD:
      • обновление сервиса:
        • Kubernetes Deployment,
        • ECS service,
        • Render/Heroku-like платформа, где деплой — это указание Dockerfile/образа;
      • миграции БД;
      • health-check, rollback при проблемах.
  • уметь описать свой вклад:

    • писал Dockerfile;
    • настраивал переменные окружения;
    • проверял, что сервис поднимается в контейнере;
    • участвовал в интеграции с Postgres/Redis в docker-compose;
    • помогал воспроизводить продакшн-проблемы локально в контейнере.

Пример: деплой через Render/аналог PaaS

Даже если опыт был с Render:

  • это тоже часть практики:
    • платформа берёт Dockerfile;
    • билдит образ;
    • поднимает контейнер;
    • вы настраиваете VARы окружения, подключения к БД.
  • Важно понимать:
    • логи смотреть в UI;
    • где health-check;
    • как обновление версии связано с git push / образами.
  1. Чего ожидают от сильного разработчика

Кратко и по сути:

  • Я уверенно работаю с Docker:
    • умею собрать и запустить приложение в контейнере;
    • использую multi-stage build для Go;
    • знаю, как организовать docker-compose для локальной разработки (app + DB + кэш).
  • В деплое участвовал:
    • настроивал Dockerfile;
    • помогал адаптировать приложение под контейнеры (env-конфиги, миграции, health-check-и);
    • понимаю типичный CI/CD-процесс.
  • Готов:
    • углубляться в инфраструктуру: Kubernetes, managed-кластеры, observability;
    • общаться с DevOps/SRE на одном языке:
      • ресурсы, readiness/liveness, логи, метрики, rollback.

Такой ответ показывает, что Docker и деплой — это не "магия из DevOps-отдела", а понятный и управляемый для тебя процесс, и что ты умеешь упаковать и подготовить свои сервисы к продакшену.

Вопрос 43. Как у тебя был организован деплой через Docker и что ты делал сам?

Таймкод: 00:35:49

Ответ собеседника: правильный. Описывает деплой на Render с одним Dockerfile без docker-compose: указывал зависимости, порт и команду запуска; в боевом проекте в основном использовал уже готовые Dockerfile.

Правильный ответ:

На такой уточняющий вопрос достаточно показать:

  • что понимаешь жизненный цикл приложения в контейнере;
  • что можешь сам собрать и запустить образ;
  • как это встраивается в деплой на PaaS/в облако;
  • какие части процесса считаешь зоной ответственности разработчика.

Развернутый, но по сути ответ:

  • В моих задачах деплой через Docker выглядел так:

    1. Подготовка Dockerfile:

      • выбор базового образа (например, golang:alpine или php:fpm-alpine);
      • копирование исходников;
      • установка зависимостей;
      • сборка бинаря (для Go) или установка PHP-зависимостей (для Symfony);
      • указание команды запуска (CMD/ENTRYPOINT);
      • открытие нужного порта через EXPOSE.

      Пример для Go-приложения:

      FROM golang:1.22-alpine AS build

      WORKDIR /app
      COPY go.mod go.sum ./
      RUN go mod download

      COPY . .
      RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app ./cmd/app

      FROM alpine:3.19
      WORKDIR /app
      COPY --from=build /app/app .
      EXPOSE 8080
      ENTRYPOINT ["./app"]
    2. Локальная проверка:

      • docker build -t myapp:local .
      • docker run -p 8080:8080 --env-file .env myapp:local
      • проверка, что:
        • сервис стартует;
        • читает конфигурацию из env;
        • корректно коннектится к БД/кэшу (если заданы DSN);
        • health-check возвращает 200.
    3. Интеграция с платформой (например, Render/аналог PaaS):

      • платформа использует Dockerfile для сборки образа;
      • в UI/конфиге задаются:
        • переменные окружения (DB_DSN, PORT и т.п.);
        • команда запуска (если не задана в Dockerfile);
        • порт, который слушает приложение (например, 8080);
      • при пуше в репозиторий:
        • запускается билд образа;
        • при успехе платформа перезапускает сервис с новым образом.
  • Что я делаю сам как разработчик в таком процессе:

    • Конструирую корректный Dockerfile:
      • минимальный образ;
      • правильные рабочие директории;
      • отсутствие лишних зависимостей;
      • конфигурация через env, а не "зашитые" значения.
    • Обеспечиваю совместимость приложения с контейнерной средой:
      • отсутствие привязки к локальным путям;
      • чтение настроек из окружения;
      • корректные таймауты, graceful shutdown.
    • Проверяю, что:
      • билд повторим: docker build работает локально и в CI;
      • контейнер поднимается и обрабатывает запросы.
    • При работе с готовыми Dockerfile:
      • понимаю, что в них происходит;
      • при необходимости могу:
        • оптимизировать (multi-stage build, убрать лишнее);
        • добавить/исправить зависимости;
        • изменить команду запуска или порт.

Кратко:

  • Да, участвовал в деплое через Docker:
    • умел написать и адаптировать Dockerfile;
    • запускал и проверял контейнер локально;
    • интегрировал это с PaaS-платформой вроде Render.
  • Понимаю, как этот подход расширяется:
    • на CI/CD-пайплайн;
    • на деплой в Kubernetes/облаке;
    • когда один и тот же образ используется во всех окружениях.

Вопрос 44. Каков у тебя опыт работы с Symfony и знаком ли ты с Laravel?

Таймкод: 00:36:32

Ответ собеседника: правильный. Сообщает, что использует Symfony около полутора лет (со стажировки и в коммерческом проекте), с Laravel знаком в основном теоретически, без существенной практики.

Правильный ответ:

Для такого вопроса важно:

  • конкретно описать опыт с Symfony: какие задачи, какие компоненты, какой уровень самостоятельности;
  • корректно обозначить знание Laravel, не завышая его;
  • показать, что опыт в одном современном фреймворке backend-уровня переносим на другой и в целом полезен для дальнейшего перехода, в том числе в Go.

Развернутый ответ мог бы выглядеть так:

  • Symfony:

    Практически работал с Symfony порядка года-полутора в реальных проектах. Из ключевого:

    • Структура приложения и архитектура:
      • работа с контроллерами, роутингом, middleware (event listeners, kernel events);
      • разделение слоёв: HTTP-слой, сервисы, репозитории, DTO/формы;
      • использование контейнера зависимостей: регистрация сервисов, autowiring, конфигурация через YAML/PHP/attributes.
    • Работа с БД и Doctrine:
      • проектирование сущностей и маппинг на таблицы PostgreSQL;
      • миграции схемы;
      • написание запросов через QueryBuilder и DQL;
      • оптимизация запросов, понимание N+1 и его устранения (join fetch, select-стратегии);
      • использование индексов и внешних ключей на уровне БД.
    • Реализация API:
      • создание REST-эндпоинтов;
      • работа с JSON-запросами/ответами;
      • валидация входящих данных (Validator component);
      • обработка ошибок и формирование корректных HTTP-кодов;
      • аутентификация и авторизация (Security component, роли).
    • Интеграции:
      • вызов внешних API (HTTP-клиенты);
      • фоновые задачи и очереди:
        • Messenger/внешние брокеры для асинхронной обработки;
        • отправка email-ов, уведомлений.
    • Инфраструктурные моменты:
      • конфигурация под разные окружения (dev/stage/prod);
      • логирование (Monolog);
      • базовая работа с контейнерами (упаковка Symfony-приложения в Docker).

    Этот опыт означает умение работать с современным каркасом, понимать жизненный цикл HTTP-запроса, проектировать слои приложения, аккуратно работать с БД и внешними сервисами. Всё это напрямую переносится на разработку сервисов на Go: те же принципы слоистости, DI (через явную композицию и интерфейсы), чёткие контракты, тестируемость.

  • Laravel:

    • Знаком с концепциями и экосистемой:
      • маршрутизация, контроллеры, Eloquent ORM, миграции, middleware;
      • Blade, очереди, события, jobs, artisan-команды.
    • Глубокой боевой практики нет, но переход с Symfony на Laravel (или наоборот) не вызывает затруднений:
      • оба фреймворка решают похожие задачи;
      • принципы DI, MVC, миграции, middleware, роутинг, валидация и работа с БД — концептуально схожи.
    • При необходимости готов за короткое время углубиться:
      • за счёт уже имеющегося опыта с Symfony и понимания паттернов backend-разработки.

Кратко:

  • В Symfony есть реальный продакшн-опыт: бизнес-логика, БД, API, интеграции, инфраструктурные моменты.
  • Laravel знаю на концептуальном уровне, понимаю, как с ним работать, и при необходимости могу быстро войти.
  • Более важно, что есть устойчивые навыки построения backend-систем: это легко переносится между фреймворками и дальше — в мир Go-сервисов, микросервисов и самостоятельной архитектуры.

Вопрос 45. Какой рабочий график тебе удобен и готов ли ты смещать рабочий день под клиента?

Таймкод: 00:37:07

Ответ собеседника: правильный. Предпочитает рабочий день примерно с 9 до 17, но готов смещать график и при необходимости работать в ночные смены.

Правильный ответ:

На такой вопрос важно показать баланс между личными предпочтениями и гибкостью под потребности команды и заказчика.

Оптимальный ответ:

  • Базово комфортен стандартный график:

    • условно с 9:00–10:00 до 17:00–18:00 по местному времени;
    • это удобно для синхронизации с командой, планирований, созвонов и стабильного ритма.
  • При этом готов к разумной гибкости:

    • смещать начало/конец дня для пересечения с часовыми поясами заказчика;
    • участвовать во внеплановых созвонах, демо, ретро, когда это требуется;
    • при необходимости участвовать в:
      • ночных/ранних назначенных работах по релизам;
      • аварийных работах (incident response), если это согласованная зона ответственности.
  • Важные моменты, которые стоит подчеркнуть:

    • готов работать по договорённому командном расписанию и придерживаться SLA;
    • отношусь к смещению графика как к рабочему инструменту, а не проблеме;
    • при регулярных смещениях (например, постоянный overlap с другим часовым поясом) ожидаю прозрачных и устойчивых договорённостей:
      • фиксированное окно пересечения;
      • предсказуемый режим, позволяющий сохранять продуктивность.

Такой ответ показывает и зрелость, и готовность подстроиться под формат работы с клиентом, не создавая впечатления конфликтности или жёсткой негибкости.

Вопрос 46. Как бы ты поступил, если клиент недоволен результатом работы и даёт негативный отзыв?

Таймкод: 00:38:59

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

Правильный ответ:

Это вопрос не про «правильные слова», а про зрелость, умение работать с ожиданиями и управлять конфликтными ситуациями. Хороший ответ должен показывать:

  • отсутствие оборонительной реакции;
  • умение структурировать проблему;
  • ориентацию на результат и прозрачную коммуникацию;
  • выводы на будущее.

Рациональный и профессиональный алгоритм действий:

  1. Спокойно принять обратную связь
  • Не спорить на эмоциях, не оправдываться сразу.
  • Подтвердить, что замечания услышаны:
    • «Спасибо, что озвучили. Давайте конкретизируем, что именно не соответствует ожиданиям.»

Цель: показать, что вы ориентированы на решение, а не на защиту эго.

  1. Уточнить конкретику и критерии

Сфокусировать разговор:

  • Что именно не устраивает:
    • функциональность не совпадает с требованиями?
    • сроки?
    • качество кода (нестабильность, баги, производительность)?
    • UX/UI, неудобство для пользователей?
    • отсутствие прозрачной коммуникации?
  • На основе чего сделан вывод:
    • реальные баги;
    • несоответствие спецификации;
    • неучтённые изменения требований;
    • неверно понятые договорённости.

Важно зафиксировать:

  • конкретные кейсы и примеры;
  • ожидаемое поведение/результат;
  • приоритеты (что критично сейчас, что можно позже).
  1. Признать ошибки, если они есть

Если проблема на вашей стороне:

  • честно признать:
    • «Да, здесь мы недоработали / неправильно поняли / не проверили».
  • не перекладывать ответственность на клиента за неясные формулировки задним числом:
    • если требования были неформальными — это тоже сигнал, что нужно было лучше их зафиксировать.

Ключ: признание ошибки + план, а не самообвинение.

  1. Предложить конкретный план исправления

Сформировать понятный action plan:

  • краткий анализ: что нужно изменить, сколько это займет;
  • приоритизация:
    • сначала критичные баги и несоответствия;
    • затем улучшения, оптимизации;
  • прозрачные сроки:
    • «К такому-то дню исправляем критичные замечания X и Y»;
    • «По Z подготовим предложение по варианту решения и оценку».

Важно: согласовать план с клиентом, чтобы у него появилось ощущение управляемости ситуации.

  1. Обеспечить прозрачную коммуникацию
  • Регулярные обновления:
    • короткие статусы: что сделано, что осталось, есть ли риски.
  • Фиксация договорённостей:
    • в письме, тикет-системе, комментариях к задаче/PR.
  • Избегать сюрпризов:
    • если что-то идёт не по плану — заранее предупредить и предложить альтернативу.
  1. Сделать выводы на будущее

Внутренний разбор (postmortem) без поиска виноватых:

  • Что пошло не так:
    • недостаточная детализация требований?
    • отсутствие проверки acceptance criteria?
    • слабые тесты?
    • плохая коммуникация по изменениям?
  • Как сделать лучше:
    • формализовать требования (user stories, acceptance criteria);
    • внедрить code review и обязательные тесты;
    • договориться о демо-интервалах (раньше показывать промежуточный результат);
    • фиксировать изменения ожиданий.

Клиенту можно коротко донести:

  • «Мы учли ситуацию и внесли изменения в процесс, чтобы подобное не повторялось.»
  1. Важный акцент
  • Не переходить в режим «клиент не прав, он токсичный»:
    • даже если часть проблем исходит от клиента, задача инженера — минимизировать эффект через уточнения, фиксацию договорённостей и прозрачность.
  • Не обещать невыполнимое ради мгновенного успокоения:
    • завышенные обещания → ещё больший негатив.

Краткая версия ответа, звучащая уверенно:

  • Сначала уточняю конкретные причины недовольства и собираю факты.
  • Признаю свои ошибки, если они есть.
  • Формирую и согласую с клиентом конкретный план исправления с приоритетами и сроками.
  • Обеспечиваю прозрачный прогресс: статусы, демо, фиксация договорённостей.
  • После решения проблемы анализирую, как улучшить процесс (требования, тестирование, коммуникацию), чтобы снизить риск повторения.

Такой подход показывает ответственность, структурное мышление и ориентацию на долгосрочные рабочие отношения, а не на формальное «отбиться от негатива».

Вопрос 47. Готов ли ты к командировкам?

Таймкод: 00:40:27

Ответ собеседника: правильный. Отвечает, что скорее не готов к командировкам.

Правильный ответ:

Это организационный вопрос, и важно ответить честно и однозначно, чтобы ожидания сошлись заранее.

Оптимальный вариант формулировки:

  • На текущем этапе я не рассматриваю частые или длительные командировки как комфортный формат работы.
  • Готов:
    • к полноценному удалённому взаимодействию;
    • к онлайн-встречам, созвонам с командой и заказчиком;
    • при необходимости — к разовым краткосрочным выездам (по предварительной договорённости), если это критично для проекта.
  • Предпочитаю, чтобы основная работа велась в формате:
    • удалённо или из офиса/хаба без регулярных поездок в другие города/страны.

Такой ответ:

  • прозрачен для работодателя;
  • не создаёт завышенных ожиданий;
  • при этом оставляет небольшое поле для редких исключений по взаимному согласию.

Вопрос 48. Какими иностранными и какими языками программирования, кроме PHP, ты владеешь или пробовал?

Таймкод: 00:40:51

Ответ собеседника: правильный. Указывает знание английского и базовое знакомство с C++, C#, Python, Go и Java.

Правильный ответ:

Здесь важно кратко и честно показать:

  • рабочий уровень иностранного языка для инженерных задач;
  • широту кругозора в языках программирования;
  • умение переносить концепции между стеками, а не просто "пробовал синтаксис".

Оптимальный развернутый ответ:

  • Иностранные языки:

    • English:
      • читаю техническую документацию, стандарты, статьи, исходный код без существенных проблем;
      • пишу технические сообщения: комментарии к коду, pull request описания, обсуждения в issue-трекере;
      • могу устно обсуждать архитектуру, дизайн API, поведение SQL-запросов и проблемы в коде, хотя иногда подбираю формулировки — уровень близок к рабочему техническому.
    • Других иностранных языков на рабочем уровне нет.
  • Языки программирования (помимо PHP):

    • Go:

      • знаком с моделью конкуренции (goroutines, channels), контекстами и отменой;
      • понимаю подход к архитектуре:
        • явные зависимости вместо магии фреймворков;
        • разделение на слои (transport/service/repository);
        • работа с БД через database/sql, миграции;
      • могу написать:
        • HTTP API;
        • простую бизнес-логику;
        • интеграцию с PostgreSQL/Redis;
      • рассматриваю Go как ключевое направление развития для backend-сервисов.

      Пример простого HTTP-обработчика на Go:

      func helloHandler(w http.ResponseWriter, r *http.Request) {
      w.WriteHeader(http.StatusOK)
      _, _ = w.Write([]byte("hello"))
      }
    • Python:

      • использовал для скриптов, утилит, простых парсеров и автоматизации;
      • знаком с базовым синтаксисом, виртуальными окружениями, использованием библиотек;
      • при необходимости могу быстро поднять сервис на FastAPI/Flask, но основной фокус остаётся на других языках.
    • C#:

      • знаком с концепциями .NET, типизацией, ООП-моделью, async/await;
      • опыт позволяет легче понимать enterprise-стиль, DI-контейнеры, middleware, что переносится и на другие стеки.
    • Java:

      • базовое знакомство с синтаксисом, ООП, подходами к серверной разработке;
      • понимание философии JVM-стека и типичных решений (Spring и т.п.);
      • знание концепций помогает понимать архитектурные идеи больших систем.
    • C++:

      • опыт на уровне обучения:
        • понимание памяти, указателей, времени жизни объектов;
        • это даёт хорошую базу для понимания низкоуровневых аспектов производительности и работы рантайма.

Краткий вывод, который должен считываться:

  • английский — достаточен для работы с документацией и технической коммуникации;
  • есть опыт и понимание нескольких языков, но фокус — на современном backend-стеке:
    • сильная база в PHP/Symfony;
    • осознанный интерес и практическое движение в сторону Go;
  • умение переносить архитектурные и инженерные принципы между языками (ООП, интерфейсы, тестирование, работа с БД, HTTP, очереди) важнее, чем знание синтаксиса каждого из них.

Вопрос 49. Есть ли у тебя вопросы о том, как у нас распределяются задачи и какие фреймворки используются на проектах?

Таймкод: 00:41:42

Ответ собеседника: правильный. Спрашивает о принципах распределения задач и стеке, получает ответ, что всё зависит от конкретного проекта, часто используются Symfony и Laravel.

Правильный ответ:

Это вопрос от кандидата, и здесь важно показать умение задавать осмысленные вопросы, которые помогают понять процессы и среду, а не только стек.

Сильный вариант поведения и формулировки:

  1. Уточнить распределение задач и ответственности

Полезные вопросы:

  • Как обычно распределяются задачи в команде:
    • есть ли явный тимлид/техлид, который декомпозирует задачи;
    • участвуют ли разработчики в оценке и планировании;
    • как происходит постановка задач: ТЗ, user stories, acceptance criteria.
  • Есть ли практики:
    • code review обязательны для всех merge-запросов;
    • парное программирование по сложным задачам;
    • регулярные техдолг/рефакторинг-слоты;
    • участие разработчиков в выборе технических решений.

Такие вопросы показывают:

  • интерес к качеству процессов;
  • желание быть вовлечённым, а не просто “делать тикеты”.
  1. Уточнить стек и типовые фреймворки

Хорошо спросить не только “какие фреймворки”, но и “в каком контексте”.

Примеры уместных уточнений:

  • Какие фреймворки и технологии чаще всего используются на ваших проектах:
    • Symfony / Laravel / Yii / Slim для PHP;
    • Go для микросервисов или высоконагруженных API;
    • какие БД: PostgreSQL, MySQL, Redis, очереди (RabbitMQ, Kafka), поисковые движки (Elasticsearch).
  • Есть ли возможность:
    • участвовать в проектах на Symfony/Go/микросервисной архитектуре;
    • постепенно переходить с более “простых” стэков (WordPress/legacy) к более сложным backend-задачам.
  1. Уточнить подход к подбору задач под уровень и развитие

Хороший вопрос:

  • Как вы подходите к распределению задач с точки зрения развития разработчика:
    • даёте ли новые типы задач (архитектурные, по производительности, интеграции);
    • можно ли постепенно брать больше ответственности (например, за модуль, сервис, решения по БД);
    • есть ли менторство/ревью от более опытных коллег.
  1. Формулировка, которая звучит профессионально

Пример, как это можно было бы ответить и спросить:

  • "Мне интересно понимать, как у вас устроено распределение задач:
    • участвуют ли разработчики в оценке и планировании,
    • есть ли практика code review и кто принимает архитектурные решения. Также хотел бы уточнить по стеку:
    • какие технологии и фреймворки сейчас наиболее востребованы на ваших проектах,
    • есть ли проекты на Symfony и Go, и есть ли возможность со временем подключаться к более технически сложным задачам (интеграции, высоконагруженные части, работа с БД и очередями)."

Такой стиль показывает:

  • осознанный интерес к процессам, качеству и развитию;
  • способность задавать правильные вопросы, важные для долгосрочного сотрудничества;
  • умение вести диалог на уровне процесса и архитектуры, а не только "какая зарплата и сколько часов".

Вопрос 50. Когда ожидать обратную связь по результатам собеседования?

Таймкод: 00:44:56

Ответ собеседника: правильный. Корректно уточняет срок получения ответа; получает информацию, что фидбэк будет на следующей неделе (ориентировочно вторник–среда).

Правильный ответ:

Завершающий вопрос о сроках обратной связи — это нормальная профессиональная практика. Здесь важно:

  • вежливо уточнить ожидания;
  • зафиксировать ориентировочный срок;
  • при необходимости уточнить формат контакта.

Оптимальная формулировка:

  • В конце интервью:
    • "Спасибо за беседу. Подскажите, пожалуйста, в какие сроки и в каком формате ожидать обратную связь по результатам?"
  • Если собеседник называет диапазон (например, "во вторник–среду следующей недели"):
    • подтвердить понимание:
      • "Отлично, спасибо, буду ждать фидбэк во вторник–среду."
  • При необходимости уточнить:
    • через какой канал придёт ответ:
      • email,
      • мессенджер,
      • контакт через рекрутера;
    • есть ли точка контакта, если ответа долго нет.

Такой подход:

  • показывает уважение к своему времени и времени компании;
  • фиксирует взаимные ожидания;
  • остаётся корректным и деловым, не создавая давления.