Постановка задачи

🎟️ Что такое Ticketmaster?

Ticketmaster - это онлайн-платформа, позволяющая пользователям приобретать билеты на концерты, театральные постановки, спортивные и другие мероприятия.

Функциональные требования

  1. В начале интервью определите функциональные и нефункциональные требования. Для пользовательских приложений функциональные требования - это формулировки вида "Пользователь может...", а нефункциональные - это характеристики системы вида "Система должна...".
  2. Приоритизируйте 3-4 ключевых функциональных требования. Все остальные требования показывают что вы обладаете продуктовым мышлением, но явно обозначьте это "за рамками задачи", чтобы интервьюер понимал, что эти пункты не входят в дизайн. Уточните, не хочет ли интервьюер увеличить/уменьшить приоритет какого-то требования. Выбор только 3-4 требований помогает оставаться сфокусированным и уложиться во временные рамки интервью.

Основные требования

  1. Пользователи могут просматривать мероприятия.
  2. Пользователи могут искать мероприятия.
  3. Пользователи могут бронировать билеты на мероприятия.

За рамками задачи

  • Пользователи могут просматривать свои бронирования.
  • Администраторы или организаторы могут добавлять мероприятия.
  • Для популярных мероприятий есть динамическое ценообразование.

Нефункциональные требования

Основные требования

  1. Система должна отдавать приоритет доступности при поиске и просмотре мероприятий и согласованности при бронировании, чтобы избежать двойных бронирований.
  2. Система должна быть масштабируемой и способной обрабатывать высокую нагрузку для популярных мероприятий, например 10 млн пользователей для одного события.
  3. Система должна обеспечивать низкую задержку поиска (< 500 мс).
  4. Система ориентирована на чтение и должна поддерживать высокую пропускную способность чтения, соотношение чтения:записи примерно 100:1.

За рамками задачи

  • Система должна защищать пользовательские данные и соответствовать GDPR.
  • Система должна быть отказоустойчивой.
  • Система должна обеспечивать безопасные транзакции для покупок.
  • Система должна быть хорошо протестирована и легко разворачиваться (CI/CD).
  • Система должна иметь регулярные резервные копии.

На доске это может выглядеть примерно так:

Требования

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

Подготовка

Планирование подхода

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

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

Проектирование API

Начнем с определения основных сущностей, это поможет спроектировать API. Пока не обязательно знать каждое поле или колонку, но если у вас уже есть представление о том, что там будет - можно это записать.

Для основных функциональных требований понадобятся следующие сущности:

  1. Event (Мероприятие): хранит основную информацию о мероприятии, включая дату, описание, тип и исполнителя или команду.
  2. User (Пользователь): представляет человека, взаимодействующего с системой. Дополнительных пояснений не требуется.
  3. Performer (Исполнитель): представляет индивидуального исполнителя или группу, выступающую или участвующую в мероприятии. Ключевые атрибуты включают имя исполнителя, краткое описание и, возможно, ссылки на работы или профили.
  4. Venue (Площадка): представляет физическое место проведения мероприятия. Каждая сущность площадки включает адрес, вместимость и конкретную карту мест, предоставляющую расположение мест, уникальное для площадки.
  5. Ticket (Билет): хранит информацию, связанную с отдельными билетами на мероприятия. Включает атрибуты, такие как идентификатор мероприятия, детали места (секция, ряд, номер места), цена и статус (доступен или продан). При создании нового мероприятия создается билет для каждого места на площадке на основе карты мест площадки. Сама карта мест хранится как часть сущности Venue (например, JSON-структура или связанная таблица, определяющая секции, ряды и номера мест вместе с координатами для отрисовки). Клиент использует эти данные карты мест в сочетании со статусом каждого билета для отрисовки интерактивного интерфейса выбора мест.
  6. Booking (Бронирование): записывает детали покупки билетов пользователем. Обычно включает идентификатор пользователя, список идентификаторов билетов, общую цену и статус бронирования (например, в процессе или подтверждено). Эта сущность ключевая для управления транзакционным аспектом процесса покупки билетов.

Можно было бы объединить данные бронирования с сущностью Ticket, но отдельная сущность Booking полезна, когда пользователь покупает несколько билетов в одной транзакции, поскольку она объединяет их в рамках одного заказа с общим статусом оплаты и общей ценой.

В реальном интервью достаточно короткого списка как выше - главное проговорить сущности и убедиться, что вы и интервьюер одинаково их понимаете.

Дальше наша цель проста: собрать дизайн, который удовлетворяет функциональным и нефункциональным требованиям. Мы идем последовательно: сначала закрываем функциональные требования, затем усиливаем дизайн нефункциональными.

API для просмотра мероприятий прост. Создаем простой GET эндпоинт, принимающий id и возвращающий детали этого мероприятия.

GET /events/:id -> Event & Venue & Performer & Ticket[]
// билеты используются для отрисовки карты мест на клиенте

В большинстве случаев API и сущности самоописательны и интервьюер сам понимает, какие данные используются в API. Вы можете уточнить, хочет ли интервьюер более подробной информации, но будьте осторожны с избыточной многословностью - нам нужно покрыть много тем, и перечисление полей объекта Event может быть не лучшим использованием времени.

Далее, для поиска нам нужен один GET эндпоинт, принимающий набор параметров поиска и возвращающий список мероприятий, соответствующих этим параметрам.

GET /events/search?keyword={keyword}&start={start_date}&end={end_date}&pageSize={page_size}&page={page_number} -> Event[]

Когда речь заходит о покупке/бронировании билета, у нас есть POST эндпоинт, который принимает список билетов и детали оплаты и возвращает bookingId.

Позже в дизайне мы превратим это в два отдельных эндпоинта - один для резервирования билета и один для подтверждения покупки, но это хорошая отправная точка.

POST /bookings/:eventId -> bookingId
{
  "ticketIds": string[],
  "paymentDetails": ...
}

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

Высокоуровневое проектирование

1. Пользователи могут просматривать мероприятия

Когда пользователь переходит на /events/:id, он должен видеть детали мероприятия включая карту мест с отображением доступности. На странице также отображаются название и описание мероприятия. Может быть представлена ключевая информация, такая как местоположение, даты мероприятия и факты об исполнителях или командах.

Мы начинаем с разметки основных компонентов для взаимодействия между клиентом и нашими сервисами. Добавим сервис мероприятий, который подключается к базе данных, хранящей данные о мероприятиях, площадках и исполнителях, описанных в основных сущностях выше. Этот сервис будет обрабатывать чтение/просмотр мероприятий.

Просмотр мероприятия

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

  1. Клиент делает REST GET запрос с id мероприятия.
  2. API-шлюз затем перенаправляет запрос в сервис мероприятий.
  3. Сервис мероприятий запрашивает в базе данных информацию о мероприятии, площадке и исполнителях и возвращает результаты клиенту.

Компоненты:

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

2. Пользователи могут искать мероприятия

Теперь у нас есть базовая функциональность для просмотра мероприятия. Но как пользователи вообще найдут мероприятия? Когда пользователи впервые открывают ваш сайт, они ожидают возможность поиска предстоящих мероприятий. Этот поиск будет параметризован на основе любой комбинации ключевых слов, артистов/команд, местоположения, даты или типа мероприятия.

Начнем с самого базового варианта - создадим простой сервис, принимающий поисковые запросы. Этот сервис подключится к вашей базе данных и будет выполнять запросы, фильтруя данные в соответствии с параметрами. У этого подхода есть проблемы, но это хорошая стартовая точка. Мы обсудим варианты оптимизации, когда будем погружаться в детали.

Поиск мероприятий

Когда пользователь ищет мероприятие:

  1. Клиент делает REST GET запрос с параметрами поиска.
  2. API-шлюз после проверки аутентификации и ограничения частоты пересылает запрос в сервис поиска.
  3. Сервис поиска запрашивает в базе данных мероприятия, соответствующие параметрам поиска, и возвращает их клиенту.

3. Пользователи могут бронировать билеты на мероприятия

Главное, чего мы стараемся избежать - это два пользователя, заплативших за один и тот же билет. Это создало бы неловкую ситуацию на мероприятии. Чтобы обработать эту проблему согласованности, нам нужно выбрать базу данных, поддерживающую транзакции, такую, как PostgreSQL. Это позволит нам гарантировать, что только один пользователь может забронировать билет за раз.

Дополнительно нам нужно реализовать надлежащие уровни изоляции и либо блокировку на уровне строк, либо оптимистичный контроль конкурентности (OCC) для полного предотвращения двойных бронирований. Мы обсудим это подробнее в разделе Погружение в детали.

Паттерн: Разрешение конфликтов
Это наглядный пример случая, когда высокая конкурентность может привести к плохим результатам, таким как двойные бронирования. Разрешение конфликтов - это паттерн, который появляется во многих задачах на проектирование систем, поэтому стоит изучить его глубже.
Подробнее о паттерне
Простое бронирование

Простая реализация бронирования

  1. Новые таблицы: сначала добавляем две новые таблицы в базу данных: Bookings и Tickets. Таблица Bookings будет хранить детали каждого бронирования, включая идентификатор пользователя, идентификаторы билетов, общую цену и статус бронирования. Таблица Tickets будет хранить детали каждого билета, включая идентификатор мероприятия, детали места, цену и статус. Таблица Tickets также будет иметь колонку booking_id, связывающую ее с таблицей Bookings.
  2. Сервис бронирований: отвечает за основную функциональность процесса бронирования билетов. Он использует таблицы Bookings и Tickets для получения, обновления или сохранения соответствующих данных. Он также взаимодействует с платежной системой для обработки платежей. После подтверждения оплаты сервис бронирования обновляет статус билета на "sold".
  3. Платежная система: внешний сервис, ответственный за обработку платежных транзакций. После обработки платежа он уведомляет сервис бронирования о статусе транзакции.

Когда пользователь бронирует билет, происходит следующее:

  1. Пользователь перенаправляется на страницу бронирования, где может ввести данные для оплаты и подтвердить бронирование.
  2. При подтверждении отправляется POST запрос на эндпоинт /bookings с выбранными идентификаторами билетов.
  3. Сервер бронирования инициирует транзакцию для:
    • проверки доступности выбранных билетов
    • обновления статуса выбранных билетов на "booked"
    • создания новой записи бронирования в таблице Bookings
  4. Если транзакция успешна, сервер бронирования возвращает успешный ответ клиенту. В противном случае, если транзакция не удалась, например, потому что другой пользователь уже забронировал билет в то же самое время, мы возвращаем информацию об ошибке клиенту.

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

Вы можете заметить, что несколько сервисов используют одну базу данных. Правило "одна база данных на сервис" часто повторяется, но это не жесткое правило. Многие крупнейшие компании мира используют общие базы данных между сервисами, когда это имеет смысл. Здесь общая база данных - правильный выбор, потому что данные тесно связаны (бронирования нуждаются в билетах, билеты нуждаются в мероприятиях), нам нужны ACID транзакции для бронирования, и разделение баз данных добавило бы сложности без реальной пользы. На собеседовании вам следует взвешивать компромиссы и принимать осмысленные решения, а не повторять архитектурные догмы.

Вы могли заметить фундаментальную проблему с этим дизайном. Пользователи могут попасть на страницу бронирования, ввести данные для оплаты и затем узнать, что билет, который они хотели, больше не доступен. Это плохой пользовательский опыт, и мы обсудим, как этого избежать чуть позже. Пока у нас простая реализация, удовлетворяющая функциональному требованию.

Потенциальные погружения в детали

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

Степень, с которой кандидат должен проактивно вести детальное обсуждение, зависит от его уровня. Например, на собеседовании уровня Middle вполне разумно, что интервьюер задает вопросы по деталям реализации. Однако на собеседованиях уровня Senior и Staff+ ожидаемый уровень инициативы и ответственности кандидата возрастает. Они должны уметь самостоятельно видеть проблемы в дизайне и предлагать решения.

1. Как улучшить опыт бронирования путем резервирования билетов?

Текущее решение технически работает, но приводит к плохому пользовательскому опыту. Никто не хочет тратить 5 минут на заполнение формы оплаты, только чтобы узнать, что билеты, которые они хотели, больше не доступны.

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

Нам нужно обеспечить, чтобы билет был зарезервирован для определенного пользователя во время оформления заказа. Также нужно обеспечить, чтобы если пользователь бросит процесс оформления, билет освобождался для покупки другими пользователями. Наконец, нужно обеспечить, чтобы при завершении оформления статус билета менялся на "sold" и бронирование подтверждалось. Вот несколько способов, как мы можем это сделать:

Реализация бронирования

Теперь, когда пользователь хочет забронировать билет:

  1. Пользователь выбирает место на интерактивной карте мест. Клиент делает POST запрос на /bookings с ticketId, связанными с этим местом.
  2. API-шлюз маршрутизирует запрос в сервис бронирований.
  3. Сервис бронирований заблокирует этот билет, используя распределенную блокировку на Redis с TTL 10 минут (столько мы будем держать билет).
  4. Сервис бронирований также создаст новую запись бронирования в базе данных со статусом "in_progress".
  5. Мы ответим пользователю только что созданным bookingId и перенаправим его на страницу оплаты.
    • Если пользователь остановится здесь, то через 10 минут блокировка автоматически освободится, и билет станет доступен для покупки другим пользователям.
  6. Пользователь производит оплату на сайте платежной системы. Платежная система обрабатывает платеж и уведомляет нас через webhook об успешной оплате.
  7. После подтверждения успешной оплаты от платежной системы webhook нашей системы получает bookingId, встроенный в метаданные платежа. С этим bookingId webhook инициирует транзакцию в базе данных для одновременного обновления таблиц Tickets и Bookings. Конкретно, статус билета, связанного с бронированием, меняется на "sold" в таблице Tickets. Одновременно соответствующая запись бронирования в таблице Bookings помечается как "completed". Обработчик webhook должен быть идемпотентным - платежная система может повторять вызовы webhook при сбое, поэтому обработка одного и того же события оплаты дважды не должна приводить к дублированию изменений состояния. Использование bookingId как ключа идемпотентности и проверка текущего статуса бронирования перед обновлением обеспечивает безопасные повторения.
  8. Теперь билет забронирован.

2. Как обработать десятки миллионов одновременных просмотров для популярных мероприятий?

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

Паттерн: Масштабирование чтения
Страницы мероприятий получают огромную нагрузку, когда билеты поступают в продажу - тысячи пользователей обновляют одну и ту же страницу мероприятия одновременно. Эта экстремальная нагрузка на чтение делает масштабирование чтения критичным и реализуется через агрессивное кэширование деталей мероприятий, информации о площадках и схем мест.
Подробнее о паттерне
Кэширование

3. Как обеспечить хороший пользовательский опыт во время мероприятий с высоким спросом с миллионами одновременных бронирований?

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

Иногда лучшее решение - не самое технически сложное. Отличительная черта Senior/Staff инженера - это способность решать бизнес-проблемы, иногда мысля вне предполагаемых ограничений. Нижеприведенные хорошее и отличное решения иллюстрируют разницу между Senior и Staff кандидатами.

Паттерн: Обновления в реальном времени
Хотя мы бы не стали использовать SSE для этого случая, многие системы включают какой-то аспект отправки обновлений в реальном времени клиенту. Мы описали все подходы в паттерне Обновления в реальном времени.
Подробнее о паттерне
Виртуальная очередь ожидания

4. Как обеспечить быстрый поиск мероприятий?

Наша текущая реализация поиска не справится. Запросы на поиск мероприятий по ключевым словам в названии, описании или других полях потребуют полного сканирования таблицы для оператора LIKE. Это может быть очень медленно, особенно с ростом количества мероприятий.

-- медленный запрос
SELECT *
FROM Events
WHERE name LIKE '%Тейлор%'
  OR description LIKE '%Тейлор%'

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

Ускорение поиска

5. Как ускорить часто повторяющиеся поисковые запросы и снизить нагрузку на поисковую инфраструктуру?

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

Итоговый дизайн

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

Что ожидается на каждом уровне?

Хорошо, мы обсудили много всего. Возникает резонный вопрос: "сколько из этого реально ожидается от меня на интервью?" Разберем по уровням.

Middle

Ширина vs глубина: от Middle кандидата чаще ожидается ширина кругозора и знаний (примерно 80% vs 20%). Вы должны собрать понятный высокоуровневый дизайн, закрывающий все функциональные требования, но многие компоненты могут оставаться абстракциями, которые вы проработали и обсудили с интервьюером на поверхностном уровне.

Проверка базовых знаний: интервьюер будет прощупывать базу, чтобы удостовериться, что вы понимаете, что делает каждый компонент. Например, добавив API-шлюз, ожидайте вопрос "что он делает" и "как работает".

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

Задача Ticketmaster:от Middle кандидата ожидается четко определенный API и модель данных, а также высокоуровневый дизайн покрывающий функциональные требования: просмотр и бронирования мероприятий. Кандидат должен быть способен решить проблему "двойных бронирований" как минимум "хорошим решением" с полем статуса, таймаутом и Cron Job.

Senior

Глубина экспертизы: от Senior кандидата ожидания смещаются к глубине - примерно 60% ширины и 40% глубины. Нужно уметь уходить в детали там, где у вас есть практический опыт. Критично продемонстрировать глубокое понимание ключевых концепций и технологий, релевантных задаче.

Продвинутый дизайн системы: вы должны быть знакомы с продвинутыми принципами проектирования систем. Например, необходимо знание того, как использовать оптимизированное для поиска хранилище данных вроде Elasticsearch для поиска мероприятий. Также ожидается понимание использования распределенной блокировки для резервирования билетов и обсуждение детальных стратегий масштабирования (допустимо, если для этого потребовались подсказки от интервьюера), включая шардирование и репликацию.

Аргументация решений: вы должны уметь ясно объяснять плюсы/минусы архитектурных решений и их влияние на масштабирование, производительность и поддерживаемость, проговаривая компромиссы.

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

Задача Ticketmaster: от Senior кандидата ожидается, что вы быстро пройдете высокоуровневый дизайн и потратите время на детальное обсуждение оптимизации поиска, обработки "двойных бронирований" (приходя к распределенной блокировке или другому качественному решению) и даже обсуждение обработки популярных мероприятий, демонстрируя глубину экспертизы в управлении масштабируемостью и надежностью при высокой нагрузке.

Staff+

Акцент на глубину: от Staff+ кандидата ожидается глубокий разбор нюансов - примерно 40% ширины и 60% глубины. Важна демонстрация того, что, даже если вы не решали именно эту задачу раньше, вы решали достаточно похожих задач в реальном мире, чтобы уверенно спроектировать решение, опираясь на опыт.

Интервьюер понимает, что вы знаете основы (REST, нормализация данных и т. п.), так что вы можете быстро пройти это на high-level дизайне и перейти к самому интересному.

Высокая проактивность: на этом уровне ожидается, что вы будете самостоятельно выявлять и решать проблемы. Это предполагает не только реагирование на проблемы по мере их возникновения, но и их прогнозирование и реализацию упреждающих решений.

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

Решение проблем: ожидаются сильные навыки решения проблем с учетом факторов масштабирования, производительности, надежности и поддерживаемости.

Задача Ticketmaster: от Staff+ кандидата ожидается высокое качество решений по сложным проблемам, которые обсуждались выше. Хорошие кандидаты глубоко погружаются как минимум в 2-3 ключевых области, демонстрируя не только профессионализм, но и инновационное мышление и способности находить оптимальные решения. Хорошим показателем вашей экспертизы является то, что интервьюер завершает дискуссию, обретя новое понимание или точку зрения.

Войдите чтобы отмечать прогресс