<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: NowInterview</title>
    <description>The latest articles on DEV Community by NowInterview (@nowinterview).</description>
    <link>https://dev.to/nowinterview</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3858241%2F587b5647-a0c4-4f88-971e-a4b5ae2b0338.png</url>
      <title>DEV Community: NowInterview</title>
      <link>https://dev.to/nowinterview</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nowinterview"/>
    <language>en</language>
    <item>
      <title>System Design: проектируем сервис заказа такси</title>
      <dc:creator>NowInterview</dc:creator>
      <pubDate>Thu, 16 Apr 2026 13:51:55 +0000</pubDate>
      <link>https://dev.to/nowinterview/system-design-proiektiruiem-siervis-zakaza-taksi-2k8a</link>
      <guid>https://dev.to/nowinterview/system-design-proiektiruiem-siervis-zakaza-taksi-2k8a</guid>
      <description>&lt;p&gt;Перевод на русский язык статьи &lt;a href="https://www.hellointerview.com/learn/system-design/problem-breakdowns/uber" rel="noopener noreferrer"&gt;Design Uber&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Видеоразбор этой задачи на русском языке можно посмотреть здесь - &lt;a href="https://youtu.be/R9B90ewl9EY" rel="noopener noreferrer"&gt;https://youtu.be/R9B90ewl9EY&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Постановка задачи
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;🚗 Что такое Uber?&lt;/p&gt;

&lt;p&gt;Uber - платформа для заказа такси, которая связывает пассажиров и &lt;br&gt;
водителей. Она позволяет пассажирам заказать такси со смартфона, &lt;br&gt;
подбирая ближайшего водителя неподалеку, который доставит их из &lt;br&gt;
места нахождения в желаемое место назначения.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Функциональные требования
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;В начале интервью определите функциональные и нефункциональные &lt;br&gt;
требования. Для пользовательских приложений функциональные &lt;br&gt;
требования - это формулировки вида “Пользователь может…”, а &lt;br&gt;
нефункциональные - это характеристики системы вида “Система &lt;br&gt;
должна…”.&lt;/p&gt;

&lt;p&gt;Приоритизируйте 3-4 ключевых функциональных требования. Все &lt;br&gt;
остальные требования показывают, что вы обладаете продуктовым &lt;br&gt;
мышлением, но явно обозначьте это “за рамками задачи”, чтобы &lt;br&gt;
интервьюер понимал, что эти пункты не входят в дизайн. Уточните, &lt;br&gt;
не хочет ли интервьюер увеличить/уменьшить приоритет какого-то &lt;br&gt;
требования. Выбор только 3-4 требований помогает оставаться &lt;br&gt;
сфокусированным и уложиться во временные рамки интервью.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Основные требования&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Пассажиры могут указать начальное и конечное местоположение и получить стоимость поездки.&lt;/li&gt;
&lt;li&gt;Пассажиры могут заказать поездку.&lt;/li&gt;
&lt;li&gt;После запроса пассажира система подбирает доступного водителя поблизости.&lt;/li&gt;
&lt;li&gt;Водители могут принять/отклонить запрос.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;За рамками задачи&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Пассажиры могут оценивать поездку после завершения, а водители могут оценивать пассажиров.&lt;/li&gt;
&lt;li&gt;Пассажиры могут заранее планировать поездки.&lt;/li&gt;
&lt;li&gt;Пассажиры могут выбирать категории поездок (например, Эконом, Комфорт).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Нефункциональные требования
&lt;/h3&gt;

&lt;p&gt;Основные требования&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Система должна обеспечивать высокую скорость подбора водителя (&amp;lt; 1 минуты до принятия запроса или отказа).&lt;/li&gt;
&lt;li&gt;Система должна обеспечивать сильную согласованность при подборе водителя, чтобы одному водителю не назначались несколько поездок одновременно.&lt;/li&gt;
&lt;li&gt;Система должна выдерживать высокую нагрузку, особенно в пиковые периоды или во время популярных событий (100k запросов в секунду из одной локации).&lt;/li&gt;
&lt;li&gt;Масштабирование - 100 млн DAU, 15 млн поездок в день&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;За рамками задачи&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Система должна обеспечивать безопасность и приватность данных пользователей и водителей, соблюдая требования государственных регуляторов.&lt;/li&gt;
&lt;li&gt;Система должна быть отказоустойчивой, с механизмом аварийного восстановления.&lt;/li&gt;
&lt;li&gt;Система должна иметь мониторинг, логирование и уведомления для быстрого обнаружения проблем.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;На доске это может выглядеть примерно так:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqmh2ubpdkuk5lmebjrgy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqmh2ubpdkuk5lmebjrgy.png" alt="Нефункциональные требования" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Подготовка
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Планирование подхода
&lt;/h4&gt;

&lt;p&gt;Прежде чем переходить к проектированию системы, важно на секунду остановиться и продумать стратегию. К счастью, для “продуктовых” задач план обычно простой: последовательно собирать дизайн, проходя по функциональным требованиям одно за другим. Так вы сохраните фокус и не утонете в деталях.&lt;/p&gt;

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

&lt;h4&gt;
  
  
  Проектирование API
&lt;/h4&gt;

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

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rider (Пассажир)&lt;/strong&gt;: пользователь, который запрашивает поездку. Содержит личные данные, контактную информацию, способы оплаты и т. п.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Driver (Водитель)&lt;/strong&gt;: пользователь, зарегистрированный как водитель. Содержит личные данные, информацию о машине (марка, модель, год), предпочтения и статус доступности.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fare (оценка стоимости)&lt;/strong&gt;: оценка стоимости поездки. Содержит точки старта и назначения, цену и ожидаемое время поездки. Эту информацию также можно просто хранить в сущности Ride, но пока мы оставим ее отдельно (здесь нет правильного или неправильного ответа).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ride (Поездка)&lt;/strong&gt;: запись о поездке от момента запроса стоимости до завершения. Содержит информацию о пассажире и водителе, машине, состоянии поездки, маршруте, конечной стоимости, а также временные метки посадки и высадки.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Location (Местоположение)&lt;/strong&gt;: актуальная позиция водителей с координатами и временем обновления. Эта сущность является ключевой для подбора водителя и отслеживания поездки.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;API для получения оценки стоимости&lt;/strong&gt; достаточно простой. Определим POST эндпоинт, который принимает текущую локацию и пункт назначения, и возвращает объект Fare с оценкой цены и времени поездки. Мы используем POST, потому что создаем новую запись о поездке в базе данных.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /fares -&amp;gt; Fare
Body: {
  pickupLocation,
  destinationLocation
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Эндпоинт заказа поездки&lt;/strong&gt;: после того как пользователь увидел оценку, он подтверждает поездку. Этот эндпоинт инициирует процесс подбора водителя и создает новую запись Ride.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /rides -&amp;gt; Ride
Body: {
  fareId
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;На этом этапе мы сопоставляем пассажира с доступным водителем поблизости. Этот процесс происходит на стороне сервера, поэтому отдельный эндпоинт не нужен.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Эндпоинт обновления местоположения водителя&lt;/strong&gt;: чтобы подобрать водителя нужно знать, где он находится в данный момент. Этот эндпоинт вызывается клиентом водителя регулярно, чтобы держать его местоположение актуальным, обновляя базу данных.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /drivers/location -&amp;gt; Success/Error
Body: {
  lat, long
}

// заметим, что driverId берется из сессии или auth-токена и не 
// передается в теле или параметрах пути запроса
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Всегда учитывайте безопасность API. Часто кандидаты передают в &lt;br&gt;
тело запроса &lt;code&gt;userId&lt;/code&gt;, метки времени или даже &lt;code&gt;оценку стоимости&lt;/code&gt;. &lt;br&gt;
Это красный флаг для интервьюера: любые данные от клиента можно &lt;br&gt;
подделать. Пользовательские данные должны приходить из сессии или &lt;br&gt;
auth-токена, метки времени должны генерироваться на сервере, а &lt;br&gt;
&lt;code&gt;оценку стоимости&lt;/code&gt; нужно получать из базы данных.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Эндпоинт принятия заказа:&lt;/strong&gt; водитель принимает заказ, после чего система обновляет статус поездки и возвращает координаты точки посадки.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;PATCH /rides/:rideId -&amp;gt; Ride
Body: {
  accept/reject
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Объект &lt;code&gt;Ride&lt;/code&gt; должен содержать информацию о точках посадки и назначения, чтобы&lt;br&gt;
клиент водителя мог отобразить ее в интерфейсе.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1g065i36xu41c32s77d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa1g065i36xu41c32s77d.png" alt="Проектирование API и сущностей" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Высокоуровневый дизайн
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Пассажиры могут указать начальное и конечное местоположение и получить стоимость поездки
&lt;/h3&gt;

&lt;p&gt;Первое что делает пассажир - отправляет запрос на стоимость поездки, указав точку назначения.&lt;/p&gt;

&lt;p&gt;Соберем минимальный набор компонентов для расчета стоимости, добавив первый сервис - сервис поездок:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy827o3jql90mn5ol8mkr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy827o3jql90mn5ol8mkr.png" alt="Вычисление стоимости поездки" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Основные компоненты для оценки стоимости:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Клиент пассажира&lt;/strong&gt;: мобильное приложение на смартфоне пассажира, которое взаимодействует с бэкендом.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API-шлюз&lt;/strong&gt;: точка входа для запросов от клиентов, отвечает за маршрутизацию, аутентификацию, ограничение запросов и т.д.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Сервис поездок&lt;/strong&gt;: управляет состоянием поездки, начиная с расчета стоимости. Он взаимодействует со сторонними картографическими API для определения расстояния и времени в пути между точками и применяет модель ценообразования компании для расчета стоимости проезда. Для целей данного интервью мы абстрагируемся от деталей этого алгоритма.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Сторонний сервис Maps API&lt;/strong&gt;: сторонний картографический API сервис (например, Google Maps) для расчета расстояния и времени в пути.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;База данных&lt;/strong&gt;: сохраняет объекты Fare.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Рассмотрим как эти компоненты взаимодействуют когда пассажир запрашивает стоимость поездки:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Пользователь вводит начальное и конечное местоположение и отправляет POST запрос на &lt;code&gt;/fares&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;API-шлюз принимает запрос, проверяет аутентификацию и ограничения, и маршрутизирует его в сервис поездок.&lt;/li&gt;
&lt;li&gt;Сервис поездок запрашивает картографический API для получения расстояния и времени и вычисляет стоимость поездки.&lt;/li&gt;
&lt;li&gt;Сервис поездок сохраняет объект Fare в базе данных.&lt;/li&gt;
&lt;li&gt;Fare возвращается через API-шлюз, и пользователь решает, делать ли заказ.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  2. Пассажиры могут заказать поездку
&lt;/h3&gt;

&lt;p&gt;После получения стоимости и времени поездки пользователь заказывает поездку. Это действие просто расширяет существующий дизайн - мы добавляем таблицу &lt;code&gt;rides&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3n4ragdd1aqkgy11ulcv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3n4ragdd1aqkgy11ulcv.png" alt="Заказ поездки" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Когда заказ на поездку приходит мы обрабатываем его следующим образом:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Пользователь заказывает поездку, отправляя POST запрос с &lt;code&gt;fareId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;API-шлюз после проверок отправляет запрос в сервис поездок.&lt;/li&gt;
&lt;li&gt;Сервис поездок создает запись &lt;code&gt;Ride&lt;/code&gt;, ссылаясь на оценку стоимости &lt;code&gt;Fare&lt;/code&gt;, и устанавливает для поездки статус &lt;code&gt;requested&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Затем запускается процесс подбора водителя (см. ниже).&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  3. После запроса пассажира система подбирает доступного водителя поблизости
&lt;/h3&gt;

&lt;p&gt;Для реализации механизма подбора водителя в наш дизайн необходимо добавить несколько новых компонентов:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Клиент водителя&lt;/strong&gt;: принимает запросы на поездки и отправляет обновления локации в сервис локаций.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Сервис локаций&lt;/strong&gt;: принимает обновления локаций, сохраняет их в базу данных.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Сервис подбора водителя&lt;/strong&gt;: обрабатывает запросы на новые поездки и выбирает оптимального водителя (по близости, рейтингу и другим факторам).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0g8nqvslyctoi3niizrg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0g8nqvslyctoi3niizrg.png" alt="Подбор водителя" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Водители постоянно (например, раз в 5 секунд) отправляют свое текущее местоположение в сервис локаций, и мы обновляем базу данных с указанием их последнего местоположения по широте и долготе. Сервис подбора водителей использует эти данные когда приходит запрос на новую поездку для поиска оптимального соответствия.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Водители могут принять/отклонить запрос
&lt;/h3&gt;

&lt;p&gt;Как только водитель будет сопоставлен с пассажиром, он сможет принять запрос на поездку. Добавим в дизайн новый компонент:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Сервис нотификаций&lt;/strong&gt;: Отвечает за отправку уведомлений в режиме реального времени водителям, когда им подобран новый запрос на поездку. Уведомления отправляются через APN (Apple Push Notification) и FCM (Firebase Cloud Messaging) для устройств iOS и Android соответственно.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjw0ba4rsl44zci8g3wev.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjw0ba4rsl44zci8g3wev.png" alt="Уведомление водителя с возможностью принять/отклонить поездку" width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Последовательность событий при этом следующая:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Сервис подбора водителя формирует список подходящих водителей и отправляет уведомление первому в списке через APN/FCM.&lt;/li&gt;
&lt;li&gt;Водитель открывает приложение и принимает запрос, отправляя PATCH запрос с &lt;code&gt;rideId&lt;/code&gt;. Если водитель отклоняет запрос, сервис уведомляет следующего.&lt;/li&gt;
&lt;li&gt;API Gateway маршрутизирует запрос в сервис поездок.&lt;/li&gt;
&lt;li&gt;Сервис поездок обновляет статус поездки на &lt;code&gt;accepted&lt;/code&gt;, устанавливает для поездки &lt;code&gt;driverId&lt;/code&gt; и возвращает водителю координаты точки посадки.&lt;/li&gt;
&lt;li&gt;Водитель использует GPS своего клиента, чтобы построить маршрут до точки посадки.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Интервьюер ожидает push‑уведомления водителям? Разбор паттерна &lt;br&gt;
&lt;strong&gt;Обновления в реальном времени&lt;/strong&gt; охватывает опции от &lt;br&gt;
long‑polling до SSE и WebSockets.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Потенциальные погружения в детали
&lt;/h2&gt;

&lt;p&gt;Когда основные функциональные требования закрыты, мы можем перейти к нефункциональным требованиям, углубляя наш дизайн там, где это необходимо.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Насколько глубоко кандидат должен погружаться в детали зависит от &amp;gt; уровня. Для Middle кандидатов нормально, если интервьюер ведет &lt;br&gt;
большую часть обсуждения. Для Senior и Staff+ ожидается больше &lt;br&gt;
инициативы: кандидат сам видит проблемы в дизайне и предлагает &lt;br&gt;
решения.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  1. Как обрабатывать частые обновления локаций водителей и эффективный поиск по близости?
&lt;/h3&gt;

&lt;p&gt;Управлять потоком обновлений локаций и выполнять быстрые запросы на поиск по локации сложно, и текущий high-level дизайн с этим не справляется. Есть две основные проблемы:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Высокая частота записей:&lt;/strong&gt; если у нас около 5 млн водителей и они отправляют локации каждые 5 секунд, это ~1 млн обновлений в секунду. Независимо от того, выберем ли мы что-то вроде DynamoDB или PostgreSQL (оба являются отличным выбором для остальной части системы), они либо не выдержат такую нагрузку, либо их придется масштабировать настолько, что они станут слишком дорогими.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Эффективность запросов:&lt;/strong&gt; без оптимизаций запросы по координатам (proximity search) требуют полного сканирования таблицы и вычисления расстояния до каждого водителя. Даже с B‑tree индексами это плохо работает для многомерных данных вроде координат.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Что можно сделать, чтобы разобраться с этими проблемами?&lt;/p&gt;

&lt;p&gt;
  Плохое решение: Прямая запись в базу и proximity‑поиск
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Плохое решение - это наш текущий high-level дизайн: записывать каждое обновление локации в базу и выполнять proximity‑поиск по этим сырым данным. Этот подход плохо масштабируется из‑за высокой частоты обновлений и делает proximity‑поиск неэффективными и медленными. Этот метод приведет к перегрузке системы, высокой&lt;br&gt;
задержке и ухудшению пользовательского опыта, что сделает его непригодным для приложения масштаба Uber.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;
  Хорошее решение: Пакетная обработка и специализированная гео‑база
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Вместо записи каждого обновления напрямую в базу мы агрегируем обновления за небольшой интервал времени и записываем их пакетами. Это снижает количество операций записи, а также повышает пропускную способность записи и уменьшает количество конфликтов.&lt;/p&gt;

&lt;p&gt;Для поиска ближайших водителей используем специализированную геопространственную базу данных с индексами, например на основе &lt;a href="https://ru.wikipedia.org/wiki/%D0%94%D0%B5%D1%80%D0%B5%D0%B2%D0%BE_%D0%BA%D0%B2%D0%B0%D0%B4%D1%80%D0%B0%D0%BD%D1%82%D0%BE%D0%B2" rel="noopener noreferrer"&gt;деревьев квадрантов (quadtrees)&lt;/a&gt;.&lt;br&gt;
Деревья квадрантов особенно хорошо подходят для двумерных пространственных данных, таких как географические координаты, поскольку они рекурсивно делят пространство на квадранты, что значительно ускоряет proximity‑поиск.&lt;/p&gt;

&lt;p&gt;Если использовать PostgreSQL, у него есть расширение&lt;br&gt;
&lt;a href="https://postgis.net/" rel="noopener noreferrer"&gt;PostGIS&lt;/a&gt;, которое позволяет использовать&lt;br&gt;
геопространственные типы и функции без необходимости отдельного хранилища.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Интервал пакетных записей приводит к задержке: данные о локациях становятся слегка устаревшими, а это ведет к ухудшению качества подбора водителей.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: In‑memory гео‑хранилище реального времени
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Мы можем устранить ограничения предыдущих решений, используя in‑memory хранилище вроде Redis, которое поддерживает геопространственные типы и команды. Это позволяет нам обрабатывать обновления местоположения водителей в режиме реального времени и эффективно выполнять proximity-поиск, одновременно минимизируя затраты на хранение за счет автоматического истечения срока действия данных.&lt;/p&gt;

&lt;p&gt;Redis использует &lt;a href="https://www.pubnub.com/guides/what-is-geohashing/" rel="noopener noreferrer"&gt;geohashing&lt;/a&gt; для кодирования широты и долготы в единое строковое значение, которое хранится в отсортированных множествах.&lt;/p&gt;

&lt;p&gt;Redis предоставляет специализированные команды, такие как&lt;br&gt;
&lt;a href="https://redis.io/commands/geoadd/" rel="noopener noreferrer"&gt;&lt;code&gt;GEOADD&lt;/code&gt;&lt;/a&gt; и &lt;code&gt;GEOSEARCH&lt;/code&gt;, которые эффективно обрабатывают обновления в реальном времени и proximity‑поиск. Команда GEOSEARCH, которая появилась в Redis 6.2, заменяет и расширяет функциональность старых команд &lt;code&gt;GEORADIUS&lt;/code&gt; и &lt;code&gt;GEORADIUSBYMEMBER&lt;/code&gt;, давая больше гибкости и улучшая производительности.&lt;/p&gt;

&lt;p&gt;Пакетная обработка больше не нужна: Redis справляется с большим потоком обновлений в реальном времени. Кроме того, Redis автоматически удаляет данные на основе заданного времени жизни (TTL), что позволяет нам сохранять только самые последние обновления местоположения и избегать ненужных затрат на хранение.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Главная проблема этого подхода - надежность. Поскольку Redis хранит все данные в памяти (in‑memory), возможны потери данных при сбое. Однако эти риски можно смягчить несколькими способами:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis persistence&lt;/strong&gt;: мы можем включить механизмы сохранения Redis, такие как RDB (Redis Database) или AOF (append-only file), чтобы периодически сохранять данные в памяти на диск.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redis Sentinel&lt;/strong&gt;: мы можем использовать Redis Sentinel для обеспечения высокой доступности. В случае выхода из строя главного узла Sentinel обеспечивает автоматическое переключение на реплику.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Даже при потере данных ущерб минимален: локации обновляются каждые 5 секунд, и система быстро восстанавливает состояние.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft0o8l01b1ptu8x78hups.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft0o8l01b1ptu8x78hups.png" alt="In‑memory гео‑хранилище реального времени" width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Как снизить перегрузку из‑за частых обновлений локаций без потери точности?
&lt;/h3&gt;

&lt;p&gt;Частые обновления локаций перегружают сеть и серверы, что может замедлять работу системы и ухудшать пользовательский опыт. Большинство кандидатов предлагают обновлять локацию водителя каждые 5 секунд или около того. Можем ли мы разумно уменьшить количество обновлений, сохраняя при этом точность?&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Адаптивные интервалы обновлений
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Мы можем решить эту проблему, внедрив адаптивные интервалы обновления локаций, которые динамически регулируют частоту обновления в зависимости от таких факторов как скорость, направление движения, близость к ожидающим запросам на&lt;br&gt;
поездку и статус водителя.&lt;/p&gt;

&lt;p&gt;Приложение водителя использует датчики устройства и определенные алгоритмы для определения оптимального интервала. Если водитель стоит или движется медленно - обновления могут отсылаться реже. И наоборот, если водитель движется быстро или часто меняет направление, обновления отправляются чаще.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Основная сложность этого подхода - корректно построить эффективный алгоритм определения оптимальной частоты обновления. Он может потребовать тщательного тестирования в несколько итераций. Но если все сделать правильно, это значительно сократит количество обновлений и повысит эффективность системы.&lt;/p&gt;



&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Не пренебрегайте клиентом, думая о своем дизайне. У многих &lt;br&gt;
кандидатов появляется привычка рисовать маленький прямоугольник &lt;br&gt;
"клиент" и двигаться дальше. Во многих случаях нам нужна логика &lt;br&gt;
на стороне клиента для повышения эффективности и масштабируемости &lt;br&gt;
нашей системы. Как вы видели, мы можем уменьшить количество&lt;br&gt;
обновлений, используя встроенные датчики и алгоритмы для &lt;br&gt;
определения оптимального интервала их отправки. Аналогичным &lt;br&gt;
образом, для сервиса загрузки файлов клиент отвечает за разбитие &lt;br&gt;
на куски и сжатие.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Как предотвратить назначение нескольких поездок одному водителю?
&lt;/h3&gt;

&lt;p&gt;Мы определили сильную согласованность при подборе водителя как ключевое нефункциональное требование. Это означает что каждый заказ посылается на рассмотрение только одному водителю, И один водитель в каждый момент времени имеет только один заказ на рассмотрении. У водителя есть 10-15 секунд на принятие/отклонение заказа, после чего система переходит к следующему водителю. Если вы рассматривали задачу проектирования &lt;a href="https://habr.com/ru/articles/1018516/" rel="noopener noreferrer"&gt;сервиса бронирования билетов&lt;/a&gt;, это очень&lt;br&gt;
похоже, поскольку мы гарантируем что билет продается только один раз, и он зарезервирован на определенное время при оформлении заказа.&lt;/p&gt;

&lt;p&gt;
  Плохое решение: Блокировка на уровне приложения и проверка таймаута
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Основная идея заключается в том, что нам нужно заблокировать водителей, чтобы предотвратить одновременную отправку нескольких запросов на поездку одному и тому же водителю. Один из подходов - использовать блокировку на уровне приложения, при которой каждый экземпляр сервиса подбора водителя помечает запрос на поездку как "locked" при его отправке водителю. Затем он запускает таймер на время блокировки. Если водитель не принимает поездку в течение этого периода, сервер снимает блокировку и делает запрос доступным для других водителей.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;У этого подхода несколько проблем:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Отсутствие координации:&lt;/strong&gt; при работе нескольких экземпляров сервиса подбора водителя централизованная координация отсутствует, что приводит к потенциальным состояниям гонки, когда два экземпляра могут одновременно попытаться заблокировать один и тот же запрос на поездку.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Несогласованное состояние блокировки:&lt;/strong&gt; если один экземпляр устанавливает блокировку и отказывает перед ее снятием (из-за сбоя или проблемы с сетью), другие экземпляры не знают об этом, что может оставить запрос на поездку в заблокированном состоянии на неопределенный срок.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Проблемы масштабирования:&lt;/strong&gt; по мере увеличения количества экземпляров проблема координации блокировок между ними становится более явной, что приводит к более высокой вероятности ошибок и несогласованностей.&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

&lt;p&gt;
  Хорошее решение: Блокировка через статус в базе данных и таймаут
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Чтобы решить проблему координации, мы можем переместить блокировку в базу данных. Это позволяет нам использовать встроенные транзакционные возможности базы данных, чтобы гарантировать, что только один экземпляр может одновременно заблокировать запрос на поездку. Когда мы отправляем запрос водителю, мы обновляем статус этого водителя на "outstanding_request". Если водитель принимает запрос, мы обновляем статус на "accepted", а если отклоняет, мы&lt;br&gt;
обновляем статус на "available". Затем мы можем использовать простой механизм таймаута в сервисе поездок, чтобы гарантировать, что блокировка будет снята, если водитель не ответит в течение 10 секунд.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Хотя мы решили проблему координации, мы по-прежнему сталкиваемся с проблемами, связанными с использованием таймаута в памяти для разблокировки, если водитель не отвечает. Если сервис поездок выйдет из строя или будет перезапущен, таймаут будет потерян, а блокировка останется на неопределенный срок. Это распространенная проблема с таймаутами в памяти, и причина их избегать, когда&lt;br&gt;
это возможно. Одним из решений является создание cron-задания, которое будет периодически запускаться для проверки наличия блокировок с истекшим сроком действия и их снятия. Это будет работать, но добавляет ненужную сложность и задерживает разблокировку запроса на поездку.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Распределенная блокировка с TTL
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Чтобы решить проблему таймаута, мы можем использовать распределенную блокировку, реализованную с помощью in-memory хранилища, такого как Redis. Когда водителю отправляется запрос на поездку, создается блокировка с уникальным идентификатором (например, &lt;code&gt;driverId&lt;/code&gt;) и TTL = 10 секунд. Сервис подбора&lt;br&gt;
водителей пытается получить блокировку &lt;code&gt;driverId&lt;/code&gt; в Redis. Если блокировка успешно получена, это означает, что ни один другой экземпляр сервиса не сможет отправить запрос на поездку тому же водителю до тех пор, пока не истечет срок действия блокировки или она не будет снята. Если водитель соглашается на поездку&lt;br&gt;
в течение 10 секунд, сервис подбора водителя обновляет статус поездки на "accepted" в базе данных, и блокировка снимается в Redis. Если водитель не соглашается на поездку, блокировка в Redis немедленно снимается и водитель становится доступным для новых запросов на поездку.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Основная проблема этого подхода - зависимость системы от доступности и производительности Redis. Нам нужны надежные стратегии мониторинга и аварийного переключения, чтобы гарантировать, что система может быстро восстановиться после&lt;br&gt;
сбоев и что блокировки не будут потеряны.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8t14psgrmgswuqtsuj6m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8t14psgrmgswuqtsuj6m.png" alt="Распределенная блокировка с TTL" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Как гарантировать, что запросы поездок не теряются в пиковые периоды?
&lt;/h3&gt;

&lt;p&gt;В периоды пиковой нагрузки система может получать большое количество запросов на поездки, которые мы не сможем обработать и они будут отклонены. Например, это часто происходит во время особых мероприятий или праздников, когда спрос резко вырастает. Нам также необходимо защититься от случаев, когда один из серверов сервиса подбора водителя выходит из строя или перезапускается, что не должно приводить к потере запросов на поездки.&lt;/p&gt;

&lt;p&gt;
  Плохое решение: Без очереди
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Самый простой подход - обрабатывать запросы на поездки по мере их поступления без какой-либо системы очередей (как это сделано в текущем дизайне).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Основная проблема этого подхода заключается в том, что он плохо масштабируется в периоды высокой нагрузки. По мере увеличения количества входящих запросов и перегрузки система начинает отбрасывать запросы, которые не может обработать, что приводит к ухудшению пользовательского опыта. Мы можем горизонтально&lt;br&gt;
масштабировать наш сервис подбора водителей, но при внезапном всплеске спроса мы не сможем масштабироваться достаточно быстро, чтобы полностью предотвратить потерю запросов.&lt;/p&gt;

&lt;p&gt;Кроме того, если один из экземпляров сервиса выходит из строя, все запросы на поездки, обрабатываемые этим экземпляром, будут потеряны. Это приведет к тому, что пассажиры будут бесконечно ждать подбора, который так и не случится.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Очередь и динамическое масштабирование
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Чтобы решить эту проблему, мы можем добавить очередь, куда попадает запрос на поездку. Сервис подбора водителей обрабатывает запросы из очереди в порядке их поступления и может масштабироваться горизонтально в зависимости от размера очереди. Этот подход также позволяет гарантировать, что ни один запрос не будет отброшен или потерян. Мы также можем разделить очереди по географическим регионам для дальнейшего повышения эффективности.&lt;/p&gt;

&lt;p&gt;Мы могли бы использовать распределенную очередь сообщений, такую ​​как Kafka, которая позволяет нам подтверждать обработку сообщения только после того, как мы успешно подобрали водителя. Таким образом, если экземпляр сервиса подбора выйдет из строя, запрос на поездку все равно будет находиться в очереди, и его подберет другой&lt;br&gt;
экземпляр. Такой подход гарантирует, что ни один запрос на поездку не будет потерян при сбое.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Основная проблема этого подхода - добавленная сложность. Нам необходимо обеспечить масштабируемость, отказоустойчивость и высокую доступность очереди. Мы можем решить эту проблему, используя managed сервис очередей, такой как Amazon SQS или Kafka, который предоставляет требуемые характеристики "из коробки". Это позволяет нам сосредоточиться на бизнес-логике нашей системы, не&lt;br&gt;
беспокоясь об инфраструктуре.&lt;/p&gt;

&lt;p&gt;Еще одна проблема в том, что обработка некоторых запросов может занимать много времени, блокируя другие "более быстрые" запросы. Это распространенная проблема с очередями FIFO, и ее можно решить, используя очередь с приоритетом. Это позволит нам определять приоритетность запросов на основе таких факторов, как&lt;br&gt;
близость водителя, рейтинг водителя, класс поездки и так далее.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8q706vl3sfqbg7vsit4y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8q706vl3sfqbg7vsit4y.png" alt="Очередь с динамическим масштабированием" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Что делать, если водитель не отвечает вовремя?
&lt;/h3&gt;

&lt;p&gt;Наша система прекрасно работает, когда водители либо принимают, либо отклоняют заявку на поездку. Но если водитель сделал перерыв и не реагирует на запросы, мы должны гарантировать, что запрос на поездку будет продолжать обрабатываться, перенаправляя запрос следующему водителю.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Процессы которые требуют реакции или действий от человека часто &lt;br&gt;
сигнализируют, что &amp;gt; мы столкнулись с паттерном &lt;strong&gt;Многошаговые &lt;br&gt;
процессы&lt;/strong&gt;. На самом деле, Uber является первоначальным автором &lt;br&gt;
проекта с открытым исходным кодом Cadence, который лег в &lt;br&gt;
основу Temporal - системы надежного исполнения, созданную &lt;br&gt;
специально для таких случаев.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
  Хорошее решение: Очередь с задержками
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Мы можем реализовать очередь с задержками, чтобы автоматически повторять запросы на поездку со следующим доступным водителем, если текущий водитель не отвечает в течение таймаута. Когда запрос на поездку отправляется водителю, мы одновременно планируем отложенное сообщение в очереди (например, Amazon SQS позволяет добавить сообщение с таймаутом видимости, в нашем случае 10 секунд). Отложенное сообщение содержит сведения о запросе и водителе, с которым первоначально связались. При обработке отложенного сообщения система проверяет, не назначена ли еще поездка. Если это так, запрос автоматически переходит к следующему водителю, одновременно планируя еще одно отложенное сообщение для нового&lt;br&gt;
водителя и так далее.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;И опять сложность - основная проблема такого подхода. Если водитель соглашается на поездку, нам необходимо убедиться, что отложенное сообщение обрабатывается корректно и не приводит к неправильному переназначению поездки. Кроме того, этот подход требует тщательной координации между очередью и сервисом подбора водителей, чтобы обеспечить согласованность и избежать состояний гонки.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Надежное исполнение (durable execution)
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Эти системы обеспечивают встроенную поддержку таймаутов, повторных попыток и управления состоянием таким образом, чтобы выдерживать сбои и перезапуски сервисов. Весь процесс подбора водителя моделируется как workflow, который может обрабатывать сложную бизнес-логику, при этом постоянно сохраняет свое состояние,&lt;br&gt;
поэтому даже в случае сбоя процесс можно возобновить с того места, где он был остановлен.&lt;/p&gt;

&lt;p&gt;Например, Temporal workflow может выглядеть так:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Отправляем запрос первому водителю.&lt;/li&gt;
&lt;li&gt;Устанавливаем таймаут на 10 секунд.&lt;/li&gt;
&lt;li&gt;Если водитель принимает - завершаем workflow.&lt;/li&gt;
&lt;li&gt;Если водитель отклоняет или таймаут истекает - автоматически переходим к следующему водителю.&lt;/li&gt;
&lt;li&gt;Продолжаем пока водитель не найден или список водителей не исчерпан.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;И опять мы добавляем дополнительную сложность, внедряя систему оркестрации workflow. Это требует от инженеров изучения новых концепций и инструментов и добавляет в систему еще один компонент, который необходимо мониторить и обслуживать.&lt;/p&gt;

&lt;p&gt;Однако преимущества гарантированного выполнения, встроенной отказоустойчивости и упрощенной бизнес-логики часто перевешивают эти проблемы, особенно для критически важных систем, где отброшенные запросы напрямую влияют на финансовые показатели и удобство пользователей.&lt;/p&gt;



&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Как дальше масштабировать систему, снижая задержку и повышая пропускную способность?
&lt;/h3&gt;

&lt;p&gt;
  Плохое решение: Вертикальное масштабирование
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Самый простой путь - вертикальное масштабирование, при котором мы увеличиваем мощность существующих серверов, добавляя больше CPU, памяти или дисков. Это быстрый и простой способ увеличить емкость, но он имеет ряд ограничений.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Это решение плохое по многим причинам. Во-первых, это дорого и требует простоя для обновления серверов. Во-вторых, мы не сможем вертикально масштабироваться бесконечно. Наконец, это решение не является отказоустойчивым. Если сервер выйдет из строя, вся система выйдет из строя. На интервью обсуждать этот вариант вряд ли стоит, поскольку для системы такого масштаба он непрактичен.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Гео-шардирование и реплики чтения
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Лучшим подходом является горизонтальное масштабирование путем добавления дополнительных серверов. Мы можем сделать это, разделив наши данные по географическому принципу и используя реплики чтения для повышения пропускной способности чтения. Важно отметить, что это не только позволяет нам масштабироваться, но и снижает задержку за счет уменьшения расстояния между клиентом и сервером. Все компоненты системы: сервисы, очереди сообщений и базы данных можно шардировать географически. Единственный случай, когда нам&lt;br&gt;
понадобится межрегиональное вычисление (например, запрос по нескольким шардам), - это когда мы выполняем proximity-поиск на границе нескольких шардов.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Главная сложность - правильное управление шардированием. Нам необходимо гарантировать, что данные распределяются равномерно по шардам и что система может обрабатывать сбои и выполнять перебалансировку. Мы можем решить это, используя согласованное хеширование для распределения данных по шардам и реализуя стратегию репликации для повышения отказоустойчивости.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;Итоговая архитектура нашей системы может выглядеть примерно так:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk7s9rnpz8avks69zrbem.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk7s9rnpz8avks69zrbem.png" alt="Финальный дизайн Uber" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Что ожидается на каждом уровне?
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  Middle
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Проверка базовых знаний&lt;/strong&gt;: интервьюер будет прощупывать базу, чтобы удостовериться, что вы понимаете, что делает каждый компонент. Например, добавив API Gateway, ожидайте вопрос "что он делает" и "как работает".&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Задача Uber:&lt;/strong&gt; от Middle кандидата ожидается четко определенный API и модель данных, а также высокоуровневый дизайн покрывающий функциональные требования. Кандидат должен указать на необходимость использования гео-пространственного индекса для ускорения поиска по местоположению, а также реализовать, по крайней мере, "хорошее решение" проблемы блокировки запроса на поездку.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senior
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Продвинутый дизайн системы&lt;/strong&gt;: вы должны быть знакомы с современными принципами проектирования систем: различными технологиями, вариантами их использования и тем, как они сочетаются друг с другом.&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;Задача Uber:&lt;/strong&gt; от Senior кандидата ожидается, что вы быстро пройдете высокоуровневый дизайн и потратите время на детальное обсуждение как минимум двух из проблем: ускорение proximity-поиска, проблему блокировки запроса на поездку или проблему пиковых нагрузок. Вы также должны быть в состоянии обсудить плюсы и минусы различных вариантов архитектуры, особенно то, как они влияют на&lt;br&gt;
масштабируемость, производительность и удобство обслуживания.&lt;/p&gt;

&lt;h3&gt;
  
  
  Staff+
&lt;/h3&gt;

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

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

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

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

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

&lt;p&gt;&lt;strong&gt;Задача Uber:&lt;/strong&gt; от Staff+ кандидата ожидается высокое качество решений по сложным проблемам, которые обсуждались выше. Хорошие кандидаты глубоко погружаются как минимум в 3+ ключевых области, демонстрируя не только профессионализм, но и инновационное мышление и способности находить оптимальные решения. Хорошим показателем вашей экспертизы является то, что интервьюер завершает дискуссию, обретя новое понимание или точку зрения.&lt;/p&gt;




&lt;p&gt;Разборы задач по System Design:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/nowinterview/system-design-proiektiruiem-sistiemu-bronirovaniia-bilietov-2e1e"&gt;Проектируем Ticketmaster, сервис бронирования билетов&lt;/a&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>architecture</category>
      <category>backend</category>
      <category>microservices</category>
    </item>
    <item>
      <title>System Design: проектируем систему бронирования билетов</title>
      <dc:creator>NowInterview</dc:creator>
      <pubDate>Fri, 03 Apr 2026 11:11:27 +0000</pubDate>
      <link>https://dev.to/nowinterview/system-design-proiektiruiem-sistiemu-bronirovaniia-bilietov-2e1e</link>
      <guid>https://dev.to/nowinterview/system-design-proiektiruiem-sistiemu-bronirovaniia-bilietov-2e1e</guid>
      <description>&lt;p&gt;Перевод на русский язык статьи &lt;a href="https://www.hellointerview.com/learn/system-design/problem-breakdowns/ticketmaster" rel="noopener noreferrer"&gt;Design Ticketmaster&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Видеоразбор этой задачи &lt;strong&gt;на русском языке&lt;/strong&gt; можно посмотреть здесь - &lt;a href="https://www.youtube.com/watch?v=zxeR5bfsNOg" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=zxeR5bfsNOg&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Постановка задачи
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎟️ Что такое Ticketmaster?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ticketmaster - это онлайн-платформа, позволяющая пользователям &lt;br&gt;
приобретать билеты на концерты, театральные постановки, &lt;br&gt;
спортивные и другие мероприятия.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Функциональные требования
&lt;/h3&gt;

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

&lt;p&gt;Основные требования&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Пользователи могут просматривать мероприятия.&lt;/li&gt;
&lt;li&gt;Пользователи могут искать мероприятия.&lt;/li&gt;
&lt;li&gt;Пользователи могут бронировать билеты на мероприятия.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;За рамками задачи&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Пользователи могут просматривать свои бронирования.&lt;/li&gt;
&lt;li&gt;Администраторы или организаторы могут добавлять мероприятия.&lt;/li&gt;
&lt;li&gt;Для популярных мероприятий есть динамическое ценообразование.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Нефункциональные требования
&lt;/h3&gt;

&lt;p&gt;Основные требования&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Система должна отдавать приоритет доступности при поиске и просмотре мероприятий и согласованности при бронировании, чтобы избежать двойных бронирований.&lt;/li&gt;
&lt;li&gt;Система должна быть масштабируемой и способной обрабатывать высокую нагрузку для популярных мероприятий, например 10 млн пользователей для одного события.&lt;/li&gt;
&lt;li&gt;Система должна обеспечивать низкую задержку поиска (&amp;lt; 500 мс).&lt;/li&gt;
&lt;li&gt;Система ориентирована на чтение и должна поддерживать высокую пропускную способность чтения, соотношение чтения:записи примерно 100:1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;За рамками задачи&lt;/p&gt;

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

&lt;p&gt;На доске это может выглядеть примерно так:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshv7xaxe22fvr3rwqeyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshv7xaxe22fvr3rwqeyq.png" alt="Нефункциональные требования" width="800" height="151"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Описание требований за рамками задачи показывает продуктовое &lt;br&gt;
мышление и дает интервьюеру возможность переопределить &lt;br&gt;
приоритеты. Но это все же необязательная вещь, если &lt;br&gt;
дополнительные идеи не приходят в голову сразу, не тратьте время &amp;gt; и двигайтесь дальше.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Подготовка
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Планирование подхода
&lt;/h3&gt;

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

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

&lt;h3&gt;
  
  
  Проектирование API
&lt;/h3&gt;

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

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Event (Мероприятие)&lt;/strong&gt;: хранит основную информацию о мероприятии, включая дату, описание, тип и исполнителя или команду.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User (Пользователь)&lt;/strong&gt;: представляет человека, взаимодействующего с системой. Дополнительных пояснений не требуется.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performer (Исполнитель)&lt;/strong&gt;: представляет индивидуального исполнителя или группу, выступающую или участвующую в мероприятии. Ключевые атрибуты включают имя исполнителя, краткое описание и, возможно, ссылки на работы или профили.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Venue (Площадка)&lt;/strong&gt;: представляет физическое место проведения мероприятия. Каждая сущность площадки включает адрес, вместимость и конкретную карту мест, предоставляющую расположение мест, уникальное для площадки.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ticket (Билет)&lt;/strong&gt;: хранит информацию, связанную с отдельными билетами на мероприятия. Включает атрибуты, такие как идентификатор мероприятия, детали места (секция, ряд, номер места), цена и статус (доступен или продан). При создании нового мероприятия создается билет для каждого места на площадке на основе карты мест площадки. Сама карта мест хранится как часть сущности Venue (например, JSON-структура или связанная таблица, определяющая секции, ряды и номера мест вместе с координатами для отрисовки). Клиент использует эти данные карты мест в сочетании со статусом каждого билета для отрисовки интерактивного интерфейса выбора мест.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Booking (Бронирование)&lt;/strong&gt;: записывает детали покупки билетов пользователем. Обычно включает идентификатор пользователя, список идентификаторов билетов, общую цену и статус бронирования (например, в процессе или подтверждено). Эта сущность ключевая для управления транзакционным аспектом процесса покупки билетов.&lt;/li&gt;
&lt;/ol&gt;

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

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

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

&lt;p&gt;API для просмотра мероприятий прост. Создаем простой GET эндпоинт, принимающий &lt;code&gt;id&lt;/code&gt; и возвращающий детали этого мероприятия.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /events/:id -&amp;gt; Event &amp;amp; Venue &amp;amp; Performer &amp;amp; Ticket[]
// билеты используются для отрисовки карты мест на клиенте
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /events/search?keyword={keyword}&amp;amp;start={start_date}&amp;amp;end={end_date}&amp;amp;pageSize={page_size}&amp;amp;page={page_number} -&amp;gt; Event[]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/bookings/:eventId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;bookingId&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticketIds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"paymentDetails"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  Высокоуровневое проектирование
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Пользователи могут просматривать мероприятия
&lt;/h3&gt;

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

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkurgnurld6gnn81n7sbm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkurgnurld6gnn81n7sbm.png" alt="Просмотр мероприятия" width="800" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Клиент делает REST GET запрос с &lt;code&gt;id&lt;/code&gt; мероприятия.&lt;/li&gt;
&lt;li&gt;API-шлюз затем перенаправляет запрос в сервис мероприятий.&lt;/li&gt;
&lt;li&gt;Сервис мероприятий запрашивает в базе данных информацию о мероприятии, площадке и исполнителях и возвращает результаты клиенту.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Компоненты:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  2. Пользователи могут искать мероприятия
&lt;/h3&gt;

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

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fton0khwmcgew856q99bh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fton0khwmcgew856q99bh.png" alt="Поиск мероприятий" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Когда пользователь ищет мероприятие:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Клиент делает REST GET запрос с параметрами поиска.&lt;/li&gt;
&lt;li&gt;API-шлюз после проверки аутентификации и ограничения частоты пересылает запрос в сервис поиска.&lt;/li&gt;
&lt;li&gt;Сервис поиска запрашивает в базе данных мероприятия, соответствующие параметрам поиска, и возвращает их клиенту.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Пользователи могут бронировать билеты на мероприятия
&lt;/h3&gt;

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

&lt;p&gt;Дополнительно нам нужно реализовать надлежащие уровни изоляции и либо блокировку на уровне строк, либо &lt;a href="https://en.wikipedia.org/wiki/Optimistic_concurrency_control" rel="noopener noreferrer"&gt;оптимистичный контроль&lt;br&gt;
конкурентности&lt;/a&gt;&lt;br&gt;
(OCC) для полного предотвращения двойных бронирований. Мы обсудим это подробнее в разделе Погружение в детали.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Это наглядный пример случая, когда высокая конкурентность может &lt;br&gt;
привести к плохим результатам, таким как двойные бронирования.&lt;br&gt;
&lt;strong&gt;Управление конкуренцией&lt;/strong&gt; - это паттерн, который появляется&lt;br&gt;
во многих задачах на проектирование систем, поэтому стоит изучить &lt;br&gt;
его глубже.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmh65os6cfuha3wzf6r3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmh65os6cfuha3wzf6r3g.png" alt="Простое бронирование" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Простая реализация бронирования&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Новые таблицы&lt;/strong&gt;: сначала добавляем две новые таблицы в базу данных: &lt;code&gt;Bookings&lt;/code&gt; и &lt;code&gt;Tickets&lt;/code&gt;. Таблица &lt;code&gt;Bookings&lt;/code&gt; будет хранить детали каждого бронирования, включая идентификатор пользователя, идентификаторы билетов, общую цену и статус бронирования. Таблица &lt;code&gt;Tickets&lt;/code&gt; будет хранить детали каждого билета, включая идентификатор мероприятия, детали места, цену и статус. Таблица &lt;code&gt;Tickets&lt;/code&gt; также будет иметь колонку &lt;code&gt;booking_id&lt;/code&gt;, связывающую ее с таблицей &lt;code&gt;Bookings&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Сервис бронирований&lt;/strong&gt;: отвечает за основную функциональность процесса бронирования билетов. Он использует таблицы &lt;code&gt;Bookings&lt;/code&gt; и &lt;code&gt;Tickets&lt;/code&gt; для получения, обновления или сохранения соответствующих данных. Он также взаимодействует с платежной системой для обработки платежей. После подтверждения оплаты сервис бронирования обновляет статус билета на "sold".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Платежная система&lt;/strong&gt;: внешний сервис, ответственный за обработку платежных транзакций. После обработки платежа он уведомляет сервис бронирования о статусе транзакции.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Когда пользователь бронирует билет, происходит следующее:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Пользователь перенаправляется на страницу бронирования, где может ввести данные для оплаты и подтвердить бронирование.&lt;/li&gt;
&lt;li&gt;При подтверждении отправляется POST запрос на эндпоинт &lt;code&gt;/bookings&lt;/code&gt; с выбранными идентификаторами билетов.&lt;/li&gt;
&lt;li&gt;Сервер бронирования инициирует транзакцию для:&lt;/li&gt;
&lt;li&gt;проверки доступности выбранных билетов&lt;/li&gt;
&lt;li&gt;обновления статуса выбранных билетов на "booked"&lt;/li&gt;
&lt;li&gt;создания новой записи бронирования в таблице &lt;code&gt;Bookings&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Если транзакция успешна, сервер бронирования возвращает успешный ответ клиенту. В противном случае, если транзакция не удалась, например, потому что другой пользователь уже забронировал билет в то же самое время, мы возвращаем информацию об ошибке клиенту.&lt;/p&gt;

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

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

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

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

&lt;blockquote&gt;
&lt;p&gt;Степень, с которой кандидат должен проактивно вести детальное &lt;br&gt;
обсуждение, зависит от его уровня. Например, на собеседовании &lt;br&gt;
уровня Middle вполне разумно, что интервьюер задает вопросы по &lt;br&gt;
деталям реализации. Однако на собеседованиях уровня Senior и &lt;br&gt;
Staff+ ожидаемый уровень инициативы и ответственности кандидата&lt;br&gt;
возрастает. Они должны уметь самостоятельно видеть проблемы в &lt;br&gt;
дизайне и предлагать решения.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  1. Как улучшить опыт бронирования путем резервирования билетов?
&lt;/h3&gt;

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

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

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

&lt;p&gt;
  Плохое решение: Долгоживущие блокировки в базе данных
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Плохое решение, которое многие кандидаты предлагают для этой проблемы - использование долгоживущих блокировок базы данных (иногда называемых "интерактивными транзакциями"). При этом подходе база данных напрямую используется для блокировки конкретной строки в таблице билетов, обеспечивая эксклюзивный доступ первому пользователю, пытающемуся забронировать билет. Это обычно делается с помощью оператора &lt;code&gt;SELECT FOR UPDATE&lt;/code&gt; в PostgreSQL, который&lt;br&gt;
блокирует выбранные строки как часть транзакции базы данных. Блокировка строки сохраняется до тех пор, пока транзакция не будет зафиксирована или откачена. В течение этого времени другие транзакции, пытающиеся выбрать ту же строку с &lt;code&gt;SELECT FOR UPDATE&lt;/code&gt;, будут заблокированы до снятия блокировки. Это гарантирует,&lt;br&gt;
что только один пользователь может обработать бронирование билета за раз.&lt;/p&gt;

&lt;p&gt;Когда речь идет о снятии блокировки, есть два случая для рассмотрения:&lt;/p&gt;

&lt;p&gt;1. Если пользователь завершает покупку, транзакция фиксируется, блокировка в базе данных снимается, и статус билета устанавливается в "booked".&lt;/p&gt;

&lt;p&gt;2. Если пользователь слишком долго тянет или бросает процесс бронирования, система должна полагаться на их последующие действия или таймауты сессии для снятия блокировки. Это вносит риск бесконечной блокировки билетов при ненадлежащей обработке.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Почему это плохая идея? Блокировки базы данных предназначены для использования на короткие периоды времени. Держать транзакцию открытой долгое время (например, 5-минут) обычно не рекомендуется. Это может неэффективно использовать ресурсы базы данных и увеличивать риск конкуренции за блокировки и риск возникновения&lt;br&gt;
взаимоблокировок. Хотя PostgreSQL поддерживает &lt;code&gt;lock_timeout&lt;/code&gt; для отказа в транзакциях, слишком долго ожидающих блокировки, это не элегантное решение для нашего случая, потому что пользователи увидят ошибку вместо того, чтобы быть поставленными в очередь. Реализация таймаута потребует управления на уровне приложения и вносит дополнительные сложности. Наконец, этот подход может плохо&lt;br&gt;
масштабироваться при высокой нагрузке, поскольку длительные блокировки могут привести к увеличению времени ожидания других пользователей и стать потенциальным узким местом производительности. Обработка крайних случаев, таких&lt;br&gt;
как сбои приложения или сетевые проблемы, становится более сложной, так как они могут оставить блокировки в неопределенном состоянии.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;
  Хорошее решение: Статус и время истечения с Cron Job
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Есть решение лучше - заблокировать билет, добавив поле &lt;code&gt;status&lt;/code&gt; и &lt;code&gt;expires_at&lt;/code&gt; в таблицу билетов. Билет может находиться в 1 из 3 состояний: "available", "reserved", "booked". Это позволяет отслеживать статус каждого билета и автоматически снимать блокировку по достижении времени истечения. Когда пользователь выбирает билет, статус меняется с "available" на "reserved", и в&lt;br&gt;
&lt;code&gt;expires_at&lt;/code&gt; записывается текущая метка времени + таймаут резервирования (например, 10 минут).&lt;/p&gt;

&lt;p&gt;Теперь подумаем, как обрабатывать разблокировку с этим подходом:&lt;/p&gt;

&lt;p&gt;1. Если пользователь завершает покупку, статус меняется на "booked", и блокировка снимается.&lt;/p&gt;

&lt;p&gt;2. Если пользователь слишком долго тянет или бросает покупку, статус меняется обратно на "available" по достижении времени истечения, и блокировка снимается. Сложная часть здесь - как обрабатывать время истечения. Мы могли бы использовать Cron Job для периодического запроса строк со статусом "reserved", где прошедшее время превышает длительность блокировки, и затем вернуть их в "available". Это намного лучше, но будет некоторая задержка между истечением времени резервирования и моментом времени когда Cron Job вернет статус строки на "available". В идеале, особенно для популярных мероприятий, блокировка должна сниматься моментально после истечения.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Подход с Cron Job имеет 2 существенных недостатка:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Задержка в разблокировке&lt;/strong&gt;: существует неотъемлемая задержка между истечением билета и выполнением Cron Job, которая ведет к неэффективности, особенно для мероприятий с высоким спросом. Билеты могут оставаться недоступными для покупки даже после истечения времени, снижая возможности бронирования.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Проблемы надежности&lt;/strong&gt;: если Cron Job отказывает или работает с задержками, это может вызвать значительные сбои в процессе бронирования билетов, которые приведут к недовольству клиентов и потенциальной потере дохода.&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Неявный статус со status и expires_at
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Мы можем сделать еще лучше, чем наше решение на основе Cron, заметив, что статус доступности любого билета - это один из двух вариантов: "available" ИЛИ "reserved", но время резервирования истекло. В таком случае мы можем создавать короткие транзакции для обновления полей в записи билета (например, изменение&lt;br&gt;
"available" на "reserved" и установка времени истечения на +10 минут). Внутри этих транзакций мы можем подтвердить, что билет доступен перед резервированием или что предыдущее резервирование истекло.&lt;/p&gt;

&lt;p&gt;Таким образом, в псевдокоде наша транзакция выглядит так:&lt;/p&gt;

&lt;p&gt;1. Начинаем транзакцию.&lt;/p&gt;

&lt;p&gt;2. Проверяем, доступен ли текущий билет: "available" ИЛИ ("reserved", но истек).&lt;/p&gt;

&lt;p&gt;3. Обновляем &lt;code&gt;status&lt;/code&gt; на "reserved", а &lt;code&gt;expires_at&lt;/code&gt; на текущее время + 10 минут.&lt;/p&gt;

&lt;p&gt;4. Фиксируем транзакцию.&lt;/p&gt;

&lt;p&gt;Это гарантирует, что только один пользователь сможет зарезервировать билет, причем билет становится доступным сразу же после истечения времени&lt;br&gt;
резервирования.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Наши операции чтения будут немного медленнее из-за необходимости фильтрации по двум значениям. Мы можем частично решить это, используя материализованные представления или другие возможности современных СУБД вместе с составным индексом. Наша таблица в базе данных также менее читабельна для других потребителей данных, поскольку некоторые резервации на самом деле истекли. Мы&lt;br&gt;
можем решить эту проблему, используя Cron Job или периодическую очистку, как рассказывалось выше, с очень важной разницей: поведение нашей системы не будет затронуто, если эта очистка задержится.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Распределенная блокировка с TTL
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Другое отличное решение - реализовать распределенную блокировку с TTL (Time To Live, время жизни) с использованием распределенной системы вроде Redis.&lt;/p&gt;

&lt;p&gt;Вы можете задаться вопросом: если PostgreSQL уже обеспечивает строгую согласованность, зачем вообще нужен Redis? Ключевая причина в том, что нам нужно временное резервирование, которое автоматически истекает. PostgreSQL изначально не поддерживает TTL на уровне строк - потребовалась бы логика истечения на&lt;br&gt;
уровне приложения (подход с Cron выше). Redis дает встроенное автоматическое истечение ключей, и поскольку он целиком находится в памяти, получение и освобождение блокировки чрезвычайно быстры при высокой конкурентности.&lt;/p&gt;

&lt;p&gt;Вот как это будет работать:&lt;/p&gt;

&lt;p&gt;1. Когда пользователь выбирает билет, берем блокировку в Redis с уникальным идентификатором (например, ID билета) с предопределенным TTL. Этот TTL действует как автоматическое время истечения блокировки.&lt;/p&gt;

&lt;p&gt;2. Если пользователь завершает покупку, статус билета в базе данных обновляется на "booked", и блокировка в Redis вручную освобождается кодом приложения до истечения TTL.&lt;/p&gt;

&lt;p&gt;3. Если TTL истекает (указывая, что пользователь не завершил покупку вовремя), Redis автоматически освобождает блокировку и билет становится доступным для бронирования другими пользователями.&lt;/p&gt;

&lt;p&gt;Теперь наша таблица &lt;code&gt;Tickets&lt;/code&gt; имеет только два состояния: "available" и "booked". Блокировка зарезервированных билетов полностью обрабатывается Redis. Ключом в Redis будет ID билета, а значение - ID пользователя. Таким образом мы можем убедиться, что при подтверждении бронирования пользователь - тот, кто зарезервировал билет.&lt;/p&gt;

&lt;p&gt;У нас также нет состояния гонки при получении блокировки: команда Redis &lt;code&gt;SET key value NX EX seconds&lt;/code&gt; атомарна, поэтому только один клиент успешно установит ключ. Для многобилетных бронирований (пользователь выбирает несколько мест) можно получать блокировки последовательно для каждого билета. Если любая блокировка не удалась, освобождаем уже полученные. Использование Lua-скрипта в&lt;br&gt;
Redis может сделать получение нескольких блокировок атомарным, если билеты хешируются на один узел Redis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Сложность при чтении&lt;/strong&gt;: поскольку резервирования живут в Redis, а не в базе данных, сервису мероприятий нужен способ показывать зарезервированные места как недоступные на карте мест. Один подход - запрашивать Redis для всех заблокированных ID билетов для данного мероприятия (используя Redis Set с ключом &lt;code&gt;event:{eventId}:reserved&lt;/code&gt;, который обновляется вместе с каждой блокировкой). Это добавляет сетевой запрос в Redis при чтении, но на практике это быстро. Альтернативно можно делать write-through статуса "reserved" в базу данных при получении блокировки, считая TTL Redis источником истины для истечения блокировки и используя периодическую очистку для удаления устаревших резервирований в базе данных. В любом случае это стоит упомянуть на&lt;br&gt;
собеседовании.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Обработка сбоев&lt;/strong&gt;: если наша распределенная блокировка по какой-либо причине выйдет из строя, будет период, когда пользовательский опыт ухудшится. Обратите внимание, что мы никогда не получим "двойное бронирование", поскольку наша база данных будет использовать OCC или блокировку на уровне строк для этого.&lt;br&gt;
Недостаток только в том, что пользователи могут получить ошибку после заполнения данных для оплаты, если кто-то их опередит. Это неприятно, но это лучше, чем когда все билеты выглядят недоступными, как было бы при сбое Cron Job в нашем&lt;br&gt;
предыдущем решении.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Истечение TTL во время оплаты&lt;/strong&gt;: что если TTL блокировки истекает во время обработки платежа? Если блокировка пользователя A истекает на 10-й минуте, но его оплата завершается на 11-й, пользователь B мог перехватить блокировку между этим. В этом редком сценарии транзакция в базе данных в шаге 7 (см. далее) не&lt;br&gt;
удастся для одного из пользователей (OCC обеспечивает, что только одна запись успешна), и мы выдаем автоматический возврат через платежную систему для неудавшегося бронирования. Установите TTL достаточно большим, чтобы минимизировать вероятность этого, и, еще лучше, рассмотрите продление блокировки при инициации оплаты.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjsy7kocbhashyvmpaoeo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjsy7kocbhashyvmpaoeo.png" alt="Реализация бронирования" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Теперь, когда пользователь хочет забронировать билет:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Пользователь выбирает место на интерактивной карте мест. Клиент делает POST запрос на &lt;code&gt;/bookings&lt;/code&gt; с &lt;code&gt;ticketId&lt;/code&gt;, связанными с этим местом.&lt;/li&gt;
&lt;li&gt;API-шлюз маршрутизирует запрос в сервис бронирований.&lt;/li&gt;
&lt;li&gt;Сервис бронирований заблокирует этот билет, используя распределенную блокировку на Redis с TTL 10 минут (столько мы будем держать билет).&lt;/li&gt;
&lt;li&gt;Сервис бронирований также создаст новую запись бронирования в базе данных со статусом "in_progress".&lt;/li&gt;
&lt;li&gt;Мы ответим пользователю только что созданным &lt;code&gt;bookingId&lt;/code&gt; и перенаправим его на страницу оплаты.

&lt;ul&gt;
&lt;li&gt;Если пользователь остановится здесь, то через 10 минут блокировка автоматически освободится, и билет станет доступен для покупки другим пользователям.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Пользователь производит оплату на сайте платежной системы. Платежная система обрабатывает платеж и уведомляет нас через webhook об успешной оплате.&lt;/li&gt;
&lt;li&gt;После подтверждения успешной оплаты от платежной системы webhook нашей системы получает &lt;code&gt;bookingId&lt;/code&gt;, встроенный в метаданные платежа. С этим &lt;code&gt;bookingId&lt;/code&gt; webhook инициирует транзакцию в базе данных для одновременного обновления таблиц &lt;code&gt;Tickets&lt;/code&gt; и &lt;code&gt;Bookings&lt;/code&gt;. Конкретно, статус билета, связанного с бронированием, меняется на "sold" в таблице &lt;code&gt;Tickets&lt;/code&gt;. Одновременно соответствующая запись бронирования в таблице &lt;code&gt;Bookings&lt;/code&gt; помечается как "completed". Обработчик webhook должен быть идемпотентным - платежная система может повторять вызовы webhook при сбое, поэтому обработка одного и того же события оплаты дважды не должна приводить к дублированию изменений состояния. Использование &lt;code&gt;bookingId&lt;/code&gt; как ключа идемпотентности и проверка текущего статуса бронирования перед обновлением обеспечивает безопасные повторения.&lt;/li&gt;
&lt;li&gt;Теперь билет забронирован.&lt;/li&gt;
&lt;/ol&gt;

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

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

&lt;blockquote&gt;
&lt;p&gt;Страницы мероприятий получают огромную нагрузку, когда билеты &lt;br&gt;
поступают в продажу - тысячи пользователей обновляют одну и ту же &lt;br&gt;
страницу мероприятия одновременно. Эта экстремальная нагрузка на &lt;br&gt;
чтение делает &lt;strong&gt;масштабирование чтения&lt;/strong&gt; критичным и реализуется &lt;br&gt;
через агрессивное кэширование деталей мероприятий, информации о &lt;br&gt;
площадках и схем мест.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;
  Отличное решение: Кэширование и балансировка нагрузки
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Кэширование:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Приоритизируйте кэширование для данных с высокой частотой чтения и низкой частотой обновления, таких как детали мероприятий (названия, даты, информация о площадках), биографии исполнителей и статичные детали площадок, такие как местоположение и вместимость. Поскольку эти данные не меняются часто, мы можем кэшировать их агрессивно, чтобы значительно минимизировать нагрузку на базу данных и удовлетворить наши требования высокой доступности.&lt;/li&gt;
&lt;li&gt;Выбирайте Redis или Memcached как in-memory хранилище данных, используя их высокую скорость для обработки больших объемов операций чтения. Стратегия кэширования read-through обеспечивает доступность данных, с чтением из базы данных в случае промаха кэша и последующим обновлением кэша.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Инвалидация и согласованность кэша&lt;/strong&gt;:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;1. Настройте триггеры базы данных для уведомления системы кэширования об изменениях данных, таких как обновления дат мероприятий или состава исполнителей, для инвалидации соответствующих записей кэша.&lt;/p&gt;

&lt;p&gt;2. Реализуйте политику TTL для записей кэша, обеспечивая периодическое обновление. Эти TTL могут быть длинными для статичных данных, таких как информация о площадках, и короткими для часто обновляемых данных, таких как доступность билетов на мероприятия.&lt;/p&gt;

&lt;p&gt;Балансировка нагрузки:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Используйте алгоритмы вроде Round Robin или Least Connections для равномерного распределения трафика между экземплярами сервисов. Реализуйте балансировку нагрузки для всех горизонтально масштабируемых сервисов. Это можно не рисовать на доске, но стоит упомянуть устно.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Горизонтальное масштабирование:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Сервис мероприятий не содержит состояния (является stateless), что позволяет нам горизонтально масштабировать его для удовлетворения спроса. Мы можем делать это, добавляя больше экземпляров сервиса и балансируя нагрузку между ними.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Одна из основных сложностей - поддержание согласованности между кэшем и базой данных. Это особенно сложно при частых обновлениях деталей мероприятий (но мы этого не ожидаем).&lt;/li&gt;
&lt;li&gt;Управление большим количеством экземпляров создает сложности. Обеспечение плавного развертывания и эффективных процедур отката добавляет операционные сложности.&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2fytdzkndnzvcqiy3bab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2fytdzkndnzvcqiy3bab.png" alt="Кэширование" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

&lt;p&gt;
  Хорошее решение: SSE для обновления мест в realtime
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Чтобы обеспечить актуальность карты мест, можем использовать Server-Sent Events (SSE) для отправки обновлений клиенту в реальном времени. Это позволит обновлять карту мест, как только место забронировано (или зарезервировано) другим пользователем, без необходимости обновления страницы. SSE - это односторонний&lt;br&gt;
канал связи между сервером и клиентом. Он позволяет серверу отправлять данные клиенту без необходимости запроса со стороны клиента.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Хотя этот подход хорошо работает для умеренно популярных мероприятий, пользовательский опыт все еще пострадает при экстремально популярных мероприятиях. В случае &lt;a href="https://www.educative.io/blog/taylor-swift-ticketmaster-meltdown" rel="noopener noreferrer"&gt;"проблемы Тейлор&lt;br&gt;
Свифт"&lt;/a&gt; карта мест заполнится сразу и пользователи окажутся в дезориентированном и ошеломленном состоянии, когда доступные места исчезнут моментально.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Виртуальная очередь ожидания
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Для экстремально популярных мероприятий мы можем реализовать управляемую администратором систему виртуальной очереди ожидания для управления доступом пользователей во время исключительно высокого спроса. Пользователи размещаются в&lt;br&gt;
этой очереди до того, как смогут увидеть страницу бронирования с актуальной картой мест. Очередь находится перед сервисом бронирования, контролируя поток пользователей, получающих доступ к интерфейсу бронирования, тем самым предотвращая перегрузку системы и улучшая пользовательский опыт. Вот как это работает на высоком уровне:&lt;/p&gt;

&lt;p&gt;1. Когда пользователь запрашивает просмотр страницы бронирования, он помещается в виртуальную очередь. Мы устанавливаем постоянное соединение (SSE или WebSocket) с клиентом и добавляем его в очередь. Сама очередь может быть реализована на Redis (используя Sorted Sets с метками времени для упорядочивания). SSE проще, поскольку нам нужна только односторонняя связь сервер-клиент для обновлений позиции, хотя WebSocket подойдет, если ожидается двусторонняя связь.&lt;/p&gt;

&lt;p&gt;2.  Периодически или по определенным критериям (например, какие-то билеты были забронированы) мы извлекаем пользователей из начала очереди и уведомляем их через их соединение, что они могут перейти к покупке билетов.&lt;/p&gt;

&lt;p&gt;3. Одновременно помечаем пользователя как "активного" в Redis (например, добавляем их ID сессии во множество &lt;code&gt;active:{eventId}&lt;/code&gt; с TTL). Сервис бронирования проверяет это множество перед разрешением любых запросов на бронирование, отклоняя пользователей, не прошедших через очередь.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Долгое время ожидания в очереди может привести к разочарованию пользователей, особенно если предполагаемое время ожидания неточно или очередь движется медленно. Отправляя обновления клиенту в реальном времени, мы можем снизить этот риск, предоставляя пользователям постоянную обратную связь об их позиции в&lt;br&gt;
очереди и предполагаемом времени ожидания.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;Хотя мы бы не стали использовать SSE для этого случая, многие системы включают какой-то аспект отправки обновлений в реальном времени клиенту. Мы описали все подходы в паттерне &lt;strong&gt;Обновления в реальном времени&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa4qllird7hpujln8fvbr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa4qllird7hpujln8fvbr.png" alt="Виртуальная очередь ожидания" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Как обеспечить быстрый поиск мероприятий?
&lt;/h3&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- медленный запрос&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%Тейлор%'&lt;/span&gt;
  &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%Тейлор%'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;
  Хорошее решение: Индексация и оптимизация SQL-запросов
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Создайте индексы на таблицах &lt;code&gt;Events&lt;/code&gt;, &lt;code&gt;Performers&lt;/code&gt; и &lt;code&gt;Venues&lt;/code&gt; для улучшения производительности запросов. Индексы позволяют быстрее извлекать данные, уменьшая количество строк для сканирования. Нужно индексировать колонки, часто используемые в поисковых запросах, такие как название мероприятия, дата мероприятия, имя исполнителя и местоположение площадки.&lt;/li&gt;
&lt;li&gt;Оптимизируйте запросы для улучшения производительности. Применяйте такие техники, как использование EXPLAIN для анализа планов выполнения запросов, избегание запросов &lt;code&gt;SELECT *&lt;/code&gt;, использование &lt;code&gt;LIMIT&lt;/code&gt; для ограничения количества возвращаемых строк. Дополнительно использование &lt;code&gt;UNION&lt;/code&gt; вместо &lt;code&gt;OR&lt;/code&gt; для объединения нескольких запросов иногда может улучшить производительность.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Стандартные индексы менее эффективны для запросов с частичным совпадением строк, например, поиск "Тейлор" вместо полного "Тейлор Свифт".&lt;/li&gt;
&lt;li&gt;Хотя индексы улучшают производительность запросов, они также могут увеличить требования к хранению и замедлить операции записи, так как каждая вставка или обновление может потребовать обновления индекса.&lt;/li&gt;
&lt;li&gt;Нужно найти правильный баланс между количеством индексов и общей производительностью базы данных, особенно учитывая разнообразные и сложные паттерны запросов в системе бронирования билетов.&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Полнотекстовые индексы в базе данных
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Мы можем расширить базовую стратегию индексации описанную выше для использования полнотекстовых индексов в нашей базе данных, если они доступны. PostgreSQL имеет встроенный полнотекстовый поиск с использованием &lt;code&gt;tsvector&lt;/code&gt; и &lt;code&gt;GIN&lt;/code&gt; индексов, а MySQL предлагает свой полнотекстовый поиск. Ни один из них не использует Lucene, на котором базируется Elasticsearch. Они делают запросы для конкретных строк вроде "Тейлор" или "Свифт" намного быстрее, чем полное сканирование таблицы с помощью &lt;code&gt;LIKE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Полнотекстовые индексы требуют дополнительного места для хранения и могут быть медленнее для запросов, чем стандартные индексы.&lt;/li&gt;
&lt;li&gt;Полнотекстовые индексы могут быть сложнее в поддержке, так как требуют специальной обработки как в запросах, так и при обслуживании базы данных.&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Полнотекстовая поисковая система
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Elasticsearch - мощная поисковая система, превосходно справляющаяся с полнотекстовым поиском, выполнением сложных запросов и обработкой объемного трафика. В своей основе Elasticsearch использует инвертированные индексы - ключевая особенность, делающая его высокоэффективным для поисковых операций. Инвертированные индексы&lt;br&gt;
сопоставляют каждое уникальное слово с документами или записями, в которых оно встречается, что значительно ускоряет поисковые запросы.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Чтобы убедиться, что данные в Elasticsearch всегда синхронизированы с данными в нашей SQL базе данных, мы можем использовать механизм Change Data Capture (CDC). Этот механизм фиксирует изменения в PostgreSQL, такие как вставки, обновления и удаления, и реплицирует их в индексы Elasticsearch.&lt;/li&gt;
&lt;li&gt;Мы можем включить функцию нечеткого поиска (fuzzy search) в Elasticsearch, которая допускает толерантность к ошибкам в поисковых запросах. Так мы можем обрабатывать опечатки и небольшие вариации в написании, такие как "Тейлор Свивт" и "Тайлер Свифт". Это было бы очень сложно сделать только с SQL базой.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Поддержание кластера Elasticsearch добавляет дополнительную инфраструктурную сложность и стоимость.&lt;/li&gt;
&lt;li&gt;Поддержание синхронизации индексов Elasticsearch с PostgreSQL может быть сложным и требует надежного механизма для обеспечения согласованности данных.&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfvoggxj7tt1clqyw91s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjfvoggxj7tt1clqyw91s.png" alt="Ускорение поиска" width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;
  Хорошее решение: Стратегии кэширования с Redis
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Мы можем использовать механизмы кэширования, такие как Redis или Memcached для хранения результатов часто выполняемых поисковых запросов. Это снижает нагрузку на поисковую инфраструктуру путем обслуживания повторяющихся запросов из кэша&lt;br&gt;
вместо многократного обращения к базе данных или поисковой системе.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Дизайн ключей&lt;/strong&gt;: создавайте ключи кэша на основе параметров поискового запроса для уникальной идентификации каждого запроса.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-To-Live (TTL)&lt;/strong&gt;: устанавливайте подходящие TTL для кэшированных данных, чтобы обеспечить актуальность и релевантность информации.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Например, запись кэша может выглядеть так:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search:keyword=Тейлор&amp;amp;start=2021-01-01&amp;amp;end=2021-12-31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;event&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;event&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;event&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ttl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;часа&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Эффективное управление инвалидацией кэша может быть сложным. Устаревшие или неактуальные данные в кэше могут приводить к отдаче пользователям неправильных результатов поиска. Проблема усугубляется, если кэшировать результаты нечеткого поиска. Можно использовать комбинацию TTL и триггеров инвалидации кэша, построенных на основе тегов кэша, для обеспечения согласованности данных.&lt;/li&gt;
&lt;li&gt;Частые промахи кэша могут приводить к повышенной нагрузке на поисковую инфраструктуру, особенно в пиковые часы.&lt;/li&gt;
&lt;/ul&gt;



&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Кэширование результатов запросов и CDN
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;К нашему удобству, Elasticsearch имеет встроенные возможности кэширования, которые можно использовать для хранения результатов частых запросов. Это снижает нагрузку на обработку запросов самой поисковой системы. Elasticsearch поддерживает кэши запросов на уровне шардов для результатов фильтров, плюс отдельный кэш запросов для кэширования полных поисковых ответов, что особенно полезно для запросов с агрегацией. Это можно использовать для адаптивных&lt;br&gt;
стратегий кэширования, когда система обучается со временем и кэширует результаты наиболее часто выполняемых запросов.&lt;/p&gt;

&lt;p&gt;Также можем использовать CDN для кэширования результатов поиска географически ближе к пользователю, снижая задержку и улучшая время ответа. Заметьте, это имеет смысл только если результаты поиска не персонализированы, то есть один и тот же поисковый запрос возвращает одни и те же результаты для всех пользователей.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Проблемы&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Обеспечение согласованности между кэшированными и актуальными данными требует сложных механизмов синхронизации. Нужно убедиться в инвалидации кэша при каждом изменении базовых данных, например, при объявлении нового мероприятия.&lt;/li&gt;
&lt;li&gt;Этот подход требует большей инфраструктурной поддержки, включая интеграцию с CDN и управление адаптивными системами кэширования.&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88l93ok1vuivn48g8oew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88l93ok1vuivn48g8oew.png" alt="Итоговый дизайн" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Что ожидается на каждом уровне?
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  Middle
&lt;/h3&gt;

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

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

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

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

&lt;h3&gt;
  
  
  Senior
&lt;/h3&gt;

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

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

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

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

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

&lt;h3&gt;
  
  
  Staff+
&lt;/h3&gt;

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

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

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

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

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

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




&lt;p&gt;Разборы задач по System Design:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/nowinterview/system-design-proiektiruiem-siervis-zakaza-taksi-2k8a"&gt;Проектируем Uber, сервис заказа такси&lt;/a&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>architecture</category>
      <category>backend</category>
      <category>microservices</category>
    </item>
  </channel>
</rss>
