<?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>Sat, 16 May 2026 16:18:13 +0000</pubDate>
      <link>https://dev.to/nowinterview/system-design-proiektiruiem-siervis-dlia-khranieniia-i-obmiena-failami-bmi</link>
      <guid>https://dev.to/nowinterview/system-design-proiektiruiem-siervis-dlia-khranieniia-i-obmiena-failami-bmi</guid>
      <description>&lt;p&gt;Видеоразбор этой задачи &lt;strong&gt;на русском языке&lt;/strong&gt; можно посмотреть здесь - &lt;br&gt;
&lt;a href="https://www.youtube.com/watch?v=Zw5A33rTlL0" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=Zw5A33rTlL0&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Больше статей и разборов по System Design: &lt;a href="https://nowinterview.ru" rel="noopener noreferrer"&gt;https://nowinterview.ru&lt;/a&gt;&lt;/p&gt;


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

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

&lt;p&gt;Dropbox - это облачный сервис, позволяющий пользователям хранить &lt;br&gt;
и обмениваться файлами. Он предоставляет безопасный и надежный &lt;br&gt;
способ хранения и доступа к файлам откуда угодно, с любого &lt;br&gt;
устройства.&lt;/p&gt;
&lt;/blockquote&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;Пользователи могут скачать файл с любого устройства.&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;/ul&gt;

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

&lt;ol&gt;
&lt;li&gt;Система должна обладать высокой доступностью (приоритет доступности над согласованностью данных).&lt;/li&gt;
&lt;li&gt;Система должна поддерживать файлы размером до 50 ГБ.&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;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%2Fx8cy5mwg2oieiagiqx3v.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%2Fx8cy5mwg2oieiagiqx3v.png" alt="Нефункциональные требования" width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Многие кандидаты испытывают трудности с компромиссом, связанным с &lt;br&gt;
теоремой CAP, при решении этой задачи. Помните, что приоритет &lt;br&gt;
согласованности над доступностью вы отдаете только в том случае, &lt;br&gt;
если каждое чтение должно получать самую последнюю запись, иначе &lt;br&gt;
система неработоспособна. Например, в приложении для торговли &lt;br&gt;
акциями, если пользователь покупает акции T в Москве, а затем &lt;br&gt;
другой пользователь немедленно пытается купить акции T в Мехико, &lt;br&gt;
вам необходимо убедиться, что первая транзакция была &lt;br&gt;
реплицирована в Мехико, прежде чем вы сможете продолжить. Однако &lt;br&gt;
для файловой системы, такой как Dropbox, допустимо, если &lt;br&gt;
пользователь в Москве загружает файл, а пользователь в Мехико не &lt;br&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;В случае Dropbox основные сущности предельно просты:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;File&lt;/strong&gt;: исходные данные, которые пользователи будут загружать, скачивать и которыми будут делиться.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FileMetadata&lt;/strong&gt;: метаданные, связанные с файлом. Они включают такую ​​информацию, как имя файла, размер, MIME-тип и пользователь, загрузивший его.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User&lt;/strong&gt;: пользователь нашей системы.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;API - основной интерфейс, через который пользователи взаимодействуют с системой. Его полезно определить с самого начала, поскольку он направляет high-level дизайн. Обычно нам нужен один эндпоинт на каждое функциональное требование.&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;/files&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="err"&gt;File&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;FileMetadata&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;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;GET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/files/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;fileId&lt;/span&gt;&lt;span class="p"&gt;}&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;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;FileMetadata&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;
проектирования. В данном случае API для загрузки и скачивания &lt;br&gt;
значительно эволюционируют, поскольку мы взвешиваем компромиссы &lt;br&gt;
различных подходов в нашем high-level дизайне (подробнее об этом &lt;br&gt;
позже). Вы можете заранее сообщить об этом интервьюеру, сказав: &lt;br&gt;
"Я собираюсь описать несколько простых API эндпоинтов, но, &lt;br&gt;
возможно, вернусь к ним и улучшу их по мере того, как мы будем &lt;br&gt;
углубляться в проектирование".&lt;/p&gt;
&lt;/blockquote&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;/files/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;fileId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;/share&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="err"&gt;User&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="err"&gt;Пользователи&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="err"&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;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;GET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/files/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;fileId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;/changes&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;FileMetadata&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;В каждом из этих запросов информация о пользователе передается в &lt;br&gt;
заголовках (через session token или auth token). Это &lt;br&gt;
распространенный паттерн, так мы можем обеспечивать &lt;br&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%2Ff3uo5krop2xvk0nvxyyp.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%2Ff3uo5krop2xvk0nvxyyp.png" alt="API и основные сущности" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Высокоуровневый дизайн
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Пользователи могут загружать файлы с любого устройства
&lt;/h3&gt;

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

&lt;ol&gt;
&lt;li&gt;Где мы храним содержимое файла (бинарные данные)?&lt;/li&gt;
&lt;li&gt;Где мы храним метаданные файла?&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Наша схема будет представлять собой простой документ и может быть примерно&lt;br&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mime_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text/plain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uploaded_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user1"&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;/p&gt;

&lt;p&gt;
  Плохое решение: Загрузка на наш сервер
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Самый простой подход - загружать файлы непосредственно на наш бэкенд-сервер (назовем его файловый сервис) и хранить их там. Наш запрос &lt;code&gt;POST /files&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%2Fhq21yg2flqk65cqrlhd0.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%2Fhq21yg2flqk65cqrlhd0.png" alt="Храним файлы на одном сервере" width="800" height="260"&gt;&lt;/a&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;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;Более эффективным подходом является хранение файла в объектном хранилище, таком как Amazon S3 или Google Cloud Storage. Когда пользователь загружает файл на наш бэкенд, мы можем отправить его в объектное хранилище и сохранить метаданные в нашей базе данных. Мы можем хранить (в теории) неограниченное количество файлов в объектном хранилище, поскольку оно само позаботится о масштабировании. Это также более надежно. Если наш сервер выйдет из строя, мы не потеряем доступ к нашим файлам. Мы также можем воспользоваться такими функциями объектного хранилища, как&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%2F4ev55k37rnl6u7woxn6x.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%2F4ev55k37rnl6u7woxn6x.png" alt="Храним файлы в объектном хранилище" width="800" height="274"&gt;&lt;/a&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;



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Загрузка напрямую в объектное хранилище
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

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

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

&lt;ol&gt;
&lt;li&gt;Запрос предварительно подписанного URL-адреса (который генерируется с помощью S3 SDK), сохранение метаданных файла в нашей базе данных со статусом "uploading".
&lt;/li&gt;
&lt;/ol&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;/files/presigned-url&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;PresignedUrl&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="err"&gt;FileMetadata&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;ol&gt;
&lt;li&gt;&lt;p&gt;Используем предварительно подписанный URL-адрес для загрузки файла в объектное хранилище непосредственно с клиентской стороны. Это осуществляется посредством PUT-запроса, где файл является телом запроса.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;После загрузки файла объектное хранилище отправит уведомление на наш бэкенд с помощью &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html" rel="noopener noreferrer"&gt;S3 Notifications&lt;/a&gt;. Затем наш бэкенд обновит метаданные файла в нашей базе данных, присвоив ему статус "uploaded".&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2Fec6ugj6cw823w5nfisbg.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%2Fec6ugj6cw823w5nfisbg.png" alt="Отправляем файлы напрямую в объектное хранилище" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;/p&gt;

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

&lt;h3&gt;
  
  
  2. Пользователи могут скачать файл с любого устройства
&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;/p&gt;



&lt;/p&gt;

&lt;p&gt;
  Хорошее решение: Скачивание c объектного хранилища
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Запрос предварительно подписанного URL-адреса для скачивания файла.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;GET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/files/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;fileId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;/presigned-url&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;PresignedUrl&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Используем предварительно подписанный URL-адрес для скачивания файла из объектного хранилища непосредственно на клиентское устройство.&lt;/li&gt;
&lt;/ol&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%2Fadnws19yow7hx9u5vxy3.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%2Fadnws19yow7hx9u5vxy3.png" alt="Скачиваем файлы напрямую из объектного хранилища" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

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

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



&lt;/p&gt;

&lt;p&gt;
  Отличное решение: Скачивание c CDN
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

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

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

&lt;p&gt;В целях безопасности, как и в случае с предварительно подписанными URL-адресами S3, мы можем сгенерировать URL-адрес, который пользователь сможет использовать для скачивания файла с CDN. Этот URL-адрес предоставит пользователю разрешение на скачивание файла из определенного места в CDN в течение ограниченного времени. Подробнее об этом далее в детальном обсуждении безопасности.&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%2F3lq96gak9jod6e0ahs92.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%2F3lq96gak9jod6e0ahs92.png" alt="Скачиваем файлы из CDN" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

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

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



&lt;/p&gt;

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

&lt;p&gt;Для завершения функциональных требований нам необходимо обеспечить поддержку обмена файлами с другими пользователями. Мы реализуем это аналогично Google Drive, где вам нужно всего лишь ввести адрес электронной почты пользователя, с которым вы хотите поделиться файлом. Можно предположить, что пользователи уже авторизованы.&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;
&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mime_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text/plain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uploaded_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sharelist"&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="s2"&gt;"user2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user3"&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="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;p&gt;Когда пользователь открывает наш сайт, он ожидает увидеть список всех своих файлов и файлов, которыми с ним поделились. Получить список файлов легко - мы можем использовать индекс по полю &lt;code&gt;uploaded_by&lt;/code&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;code&gt;sharelist&lt;/code&gt; в метаданных, кэшировать список, отображающий обратную зависимость. Это будет сопоставление любого конкретного пользователя со списком файлов, которыми с ним поделились. Таким образом, когда пользователь открывает наш сайт, мы можем быстро получить список файлов, которыми с ним поделились, найдя его &lt;code&gt;user_id&lt;/code&gt; в нашем кэше &lt;code&gt;sharedFiles&lt;/code&gt;.&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;user&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"file1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file2"&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;p&gt;Нам необходимо синхронизировать список &lt;code&gt;sharedFiles&lt;/code&gt; со списком &lt;code&gt;sharelist&lt;/code&gt; в метаданных файла. Лучший способ решить эту проблему - хранить сопоставление пользователей и файлов в той же базе данных и обновлять как &lt;code&gt;sharelist&lt;/code&gt;, так и &lt;code&gt;sharedFiles&lt;/code&gt; в рамках одной транзакции.&lt;/p&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;Другой подход заключается в полной нормализации данных. Он предполагает создание новой таблицы, в которой &lt;code&gt;user_id&lt;/code&gt; сопоставляется с &lt;code&gt;file_id&lt;/code&gt;, где &lt;code&gt;file_id&lt;/code&gt; идентификатор файла, которым поделились с данным пользователем. Таким образом,&lt;br&gt;
когда пользователь открывает наш сайт, мы можем быстро получить список предоставленных ему файлов, запросив таблицу &lt;code&gt;SharedFiles&lt;/code&gt; и найдя все файлы с &lt;code&gt;user_id&lt;/code&gt; этого пользователя.&lt;/p&gt;

&lt;p&gt;Новая таблица &lt;code&gt;SharedFiles&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;| user_id (PK) | file_id (SK) |
|--------------|--------------|
| user1        | file1        |
| user1        | file2        |
| user2        | file3        |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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



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

&lt;h3&gt;
  
  
  4. Файлы синхронизируются между устройствами
&lt;/h3&gt;

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

&lt;ol&gt;
&lt;li&gt;Локально -&amp;gt; Удаленно&lt;/li&gt;
&lt;li&gt;Удаленно -&amp;gt; Локально&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Локально -&amp;gt; Удаленно&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Для этого нам нужен агент синхронизации на стороне клиента, который:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Отслеживает изменения в локальной папке Dropbox, используя события файловой системы, специфичные для операционной системы (например, FileSystemWatcher в Windows или FSEvents в macOS).&lt;/li&gt;
&lt;li&gt;При обнаружении изменений агент ставит измененный файл в очередь на отправку.&lt;/li&gt;
&lt;li&gt;Затем агент использует наш API для загрузки файлов, чтобы отправить изменения на сервер вместе с обновленными метаданными.&lt;/li&gt;
&lt;li&gt;Конфликты разрешаются с использованием стратегии "последняя запись побеждает" - это означает, что если два пользователя редактируют один и тот же файл, будет сохранена последняя внесенная ими правка.&lt;/li&gt;
&lt;/ol&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;p&gt;&lt;strong&gt;Удаленно -&amp;gt; Локально&lt;/strong&gt;&lt;/p&gt;

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

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Опрос (Polling)&lt;/strong&gt;: клиент периодически спрашивает у сервера: "Что-нибудь изменилось с момента последней синхронизации?" Сервер обращается к базе данных, чтобы проверить, есть ли у каких-либо файлов, за которыми следит пользователь, метка времени &lt;code&gt;updated_at&lt;/code&gt;, более новая, чем время последней синхронизации. Это простой метод, но он может медленно обнаруживать изменения и расходовать ресурсы, если ничего не изменилось.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;WebSocket или SSE&lt;/strong&gt;: сервер поддерживает открытое соединение с каждым клиентом и отправляет уведомления при возникновении изменений. Это более сложный подход, но он обеспечивает обновления в режиме реального времени.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;&lt;strong&gt;Новые файлы&lt;/strong&gt;: файлы, которые были недавно отредактированы (в течение последних нескольких часов). Для них мы поддерживаем соединение WebSocket, чтобы обеспечить синхронизацию практически в реальном времени.&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://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%2Fupioo60ggi5f94odp2gk.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%2Fupioo60ggi5f94odp2gk.png" alt="Синхронизация файлов" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Итоговый дизайн
&lt;/h3&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%2Fejv6shj7mg8hoqtu0ijd.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%2Fejv6shj7mg8hoqtu0ijd.png" alt="Итоговый дизайн" width="800" height="442"&gt;&lt;/a&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;br&gt;
скачивает эти изменения.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Балансировщик нагрузки и API-шлюз&lt;/strong&gt;: отвечает за маршрутизацию запросов к соответствующему серверу и обработку таких операций, как завершение SSL-соединения, ограничение скорости и проверка запросов.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Файловый сервис&lt;/strong&gt;: отвечает за запись в базу данных метаданных файлов, а также за генерацию предварительно подписанных URL-адресов с использованием SDK S3. Он фактически не обрабатывает загрузку или скачивание файлов. Это всего лишь посредник между клиентом и S3.&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;CDN&lt;/strong&gt;: кэширует файлы вблизи пользователя для уменьшения задержки.&lt;/p&gt;
&lt;h2&gt;
  
  
  Потенциальные погружения в детали
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Как поддержать большие файлы?
&lt;/h3&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Индикатор выполнения&lt;/strong&gt;: пользователи должны иметь возможность видеть ход загрузки, чтобы понимать, что она выполняется и сколько времени это займет.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Возобновляемая загрузка&lt;/strong&gt;: пользователи должны иметь возможность приостанавливать и возобновлять загрузку. В случае потери интернет-соединения или закрытия браузера они должны иметь возможность продолжить с того места, где остановились, вместо того, чтобы повторно загружать 49 ГБ, которые, возможно, уже были загружены до прерывания.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Таймауты&lt;/strong&gt;: веб-серверы и клиенты обычно имеют настройки таймаутов, чтобы предотвратить бесконечное ожидание ответа. Один POST запрос с файлом размером 50 ГБ может легко превысить эти таймауты. На самом деле, это может быть подходящим моментом для быстрых подсчетов на собеседовании. Если у нас есть файл размером 50 ГБ и интернет-соединение со скоростью 100 Мбит/с, сколько времени потребуется для загрузки файла? &lt;code&gt;50 ГБ * 8 бит/байт / 100 Мбит/с = 4000 секунд&lt;/code&gt;, тогда &lt;code&gt;4000 секунд / 60 секунд/минута / 60 минут/час = 1,11 часа&lt;/code&gt;. Это очень долгое время ожидания без ответа от сервера.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ограничения браузера и сервера&lt;/strong&gt;: в большинстве случаев загрузка файла размером 50 ГБ с помощью одного POST запроса невозможна в принципе из-за ограничений, которые браузеры и веб-серверы часто устанавливают на размер тела запроса. Хотя веб-серверы, такие как Apache и NGINX, могут быть настроены на прием больших объемов данных, большинство современных сервисов, таких как Amazon API Gateway, имеют жесткие ограничения, которые намного ниже и не могут быть увеличены. В случае с Amazon API Gateway, это всего 10 МБ.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Сетевые сбои&lt;/strong&gt;: большие файлы более подвержены сетевым сбоям. Если пользователь загружает файл размером 50 ГБ, и его интернет-соединение обрывается, ему придется начинать загрузку заново.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Пользовательский опыт&lt;/strong&gt;: пользователи фактически не видят хода загрузки. Они понятия не имеют, сколько времени это займет или идет ли вообще процесс.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Для решения этих проблем мы можем использовать метод, называемый "разбивкой на части" (chunking), чтобы разбить файл на более мелкие фрагменты и загружать их по одному (или параллельно, в зависимости от пропускной способности сети). Распространенная ошибка, которую допускают кандидаты, - это разбивка файла на части на сервере, в чем фактически нет смысла, поскольку для этого все равно загружается весь файл целиком. Поэтому разбивка должна выполняться на стороне клиента. Обычно мы разбиваем файл на фрагменты размером 5-10 МБ, но это можно скорректировать в зависимости от условий сети и размера файла.&lt;/p&gt;

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

&lt;p&gt;Следующий вопрос: &lt;strong&gt;как мы будем обрабатывать возобновляемые загрузки?&lt;/strong&gt; Нам нужно отслеживать, какие фрагменты были загружены, а какие нет. Мы можем сделать это, сохраняя состояние загрузки в базе данных, а именно в нашей таблице &lt;code&gt;FileMetadata&lt;/code&gt;. Давайте обновим схему &lt;code&gt;FileMetadata&lt;/code&gt; , добавив поле &lt;code&gt;chunks&lt;/code&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mimeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text/plain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uploadedBy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uploading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chunks"&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="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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chunk1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uploaded"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chunk2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uploading"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chunk3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not-uploaded"&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="p"&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;Когда пользователь возобновляет загрузку, мы можем проверить поле "chunks", чтобы увидеть, какие фрагменты уже загружены, а какие нет. Затем мы можем начать загрузку тех фрагментов, которые еще не были загружены. Таким образом, пользователю не придется начинать загрузку заново, если он потеряет интернет-соединение или закроет браузер.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Но как нам обеспечить синхронизацию поля &lt;code&gt;chunks&lt;/code&gt; с фактически загруженными фрагментами файла?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Мы можем использовать два подхода:&lt;/p&gt;

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

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

&lt;ol&gt;
&lt;li&gt;Клиент берет файл, разбивает его на фрагменты и загружает эти фрагменты непосредственно в S3.&lt;/li&gt;
&lt;li&gt;S3 отправляет сообщение об успешной загрузке каждого фрагмента.&lt;/li&gt;
&lt;li&gt;В случае успеха клиент отправляет PATCH запрос на наш бэкенд для обновления поля &lt;code&gt;chunks&lt;/code&gt; в таблице &lt;code&gt;FileMetadata&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;PATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/files/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;fileId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;/chunks&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;"chunks"&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="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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chunk1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uploaded"&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="p"&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;p&gt;Проблема заключается в том, что мы полагаемся на клиента в вопросе синхронизации поля &lt;code&gt;chunks&lt;/code&gt; с фактически загруженными фрагментами, что представляет собой риск безопасности. Злоумышленник может отправить PATCH запрос на наш бэкенд, чтобы пометить все фрагменты как загруженные, не загружая их фактически. Хотя в этом случае он сможет повредить только свой собственный загруженный файл, а не чей-либо еще, это все равно риск, который может привести к несогласованному состоянию, которое трудно отладить. Мы можем решить эту проблему, используя сервер для синхронизации поля &lt;code&gt;chunks&lt;/code&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;Более эффективным подходом является реализация серверной проверки загрузки фрагментов с использованием ETags. Уведомления о событиях S3 не срабатывают для отдельных фрагментов загрузки, a только когда весь объект завершен. Поэтому нам необходимо использовать непосредственно API многокомпонентной загрузки S3 (S3 Multipart Upload API).&lt;/p&gt;

&lt;p&gt;Каждый фрагмент получает ETag после успешной загрузки, который клиент может включить в PATCH запрос к нашему бэкенду. Затем наш бэкенд может проверить эти ETag, вызвав ListParts API в S3, что обеспечивает эффективный способ проверки нескольких фрагментов одновременно. Такой подход обеспечивает баланс между удобством использования и целостностью данных - мы принимаем обновления от&lt;br&gt;
клиента для отслеживания прогресса в реальном времени, чтобы предоставлять немедленную обратную связь, но периодически проверяем статус фрагмента на стороне сервера, прежде чем пометить весь файл как "uploaded".&lt;/p&gt;

&lt;p&gt;Доверяй, но проверяй.&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;Далее поговорим о том, как однозначно идентифицировать файл и его фрагмент. Когда вы пытаетесь возобновить загрузку, первый вопрос, который следует задать, это: (1) Пытались ли мы загрузить этот файл раньше? и (2) Если да, то какие фрагменты уже загружены? Чтобы ответить на первый вопрос, мы не можем наивно полагаться на имя файла. Это связано с тем, что два разных пользователя (или&lt;br&gt;
даже один и тот же пользователь) могут загружать файлы с одинаковым именем. Вместо этого нам нужно полагаться на уникальный идентификатор, полученный из содержимого файла. Это называется отпечатком (fingerprint).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Fingerprint_(computing)" rel="noopener noreferrer"&gt;Отпечаток&lt;/a&gt; - это&lt;br&gt;
результат математического вычисления, которое генерирует уникальное хеш-значение на основе содержимого файла. Это хеш-значение, часто создаваемое с помощью криптографических хеш-функций, таких как SHA-256, служит надежным и уникальным идентификатором файла независимо от его имени или источника загрузки. Вычислив этот отпечаток, мы можем быстро и достоверно определить, был ли файл или какая-либо его часть загружены ранее.&lt;/p&gt;

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

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

&lt;ol&gt;
&lt;li&gt;Клиент разбивает файл на части размером 5-10 МБ и вычисляет отпечаток для каждой части. Он также вычисляет отпечаток для всего файла, который станет идентификатором файла (&lt;code&gt;fileId&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Клиент отправляет GET запрос для получения &lt;code&gt;FileMetadata&lt;/code&gt; с заданным &lt;code&gt;fileId&lt;/code&gt;(отпечатком), чтобы проверить, существует ли он уже - в этом случае мы сможем возобновить загрузку.&lt;/li&gt;
&lt;li&gt;Если файл не существует, клиент отправляет POST запрос для инициирования загрузки (multipart upload). Бэкенд вызывает S3 API CreateMultipartUpload, чтобы получить &lt;code&gt;uploadId&lt;/code&gt;, генерирует предварительно подписанные URL-адреса для каждой части, сохраняет метаданные файла в таблице &lt;code&gt;FileMetadata&lt;/code&gt; со статусом "uploading" и возвращает &lt;code&gt;uploadId&lt;/code&gt; вместе с предварительно подписанными URL-адресами для каждого фрагмента.&lt;/li&gt;
&lt;li&gt;Затем клиент загружает каждый фрагмент в S3, используя соответствующий предварительно подписанный URL-адрес (для каждой части требуется свой собственный предварительно подписанный URL-адрес с идентификатором загрузки &lt;code&gt;uploadId&lt;/code&gt; и номером части &lt;code&gt;partNumber&lt;/code&gt;). После загрузки каждого фрагмента клиент отправляет PATCH запрос в наш бэкенд со статусом фрагмента и ETag. Затем наш бэкенд может проверить загрузку фрагментов с помощью S3 API ListParts, прежде чем обновить поле &lt;code&gt;chunks&lt;/code&gt; в таблице &lt;code&gt;FileMetadata&lt;/code&gt;, и помечает фрагмент как "uploaded".&lt;/li&gt;
&lt;li&gt;Как только все фрагменты в нашем массиве фрагментов будут помечены как "uploaded", бэкенд обновляет таблицу &lt;code&gt;FileMetadata&lt;/code&gt; и помечает весь файл как "uploaded".&lt;/li&gt;
&lt;/ol&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Описанный нами подход не нов, на самом деле, эта проблема уже &lt;br&gt;
решена поставщиками облачных хранилищ, такими как Amazon S3. У &lt;br&gt;
них есть функция &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html" rel="noopener noreferrer"&gt;Multipart Upload&lt;/a&gt;,&lt;br&gt;
которая позволяет загружать большие объекты по частям. Это именно &lt;br&gt;
то, что мы только что описали. Клиент разбивает файл на части и &lt;br&gt;
загружает каждую часть в S3. Затем S3 объединяет части в один &lt;br&gt;
объект. Они даже предоставляют удобный &lt;a href="https://aws.amazon.com/blogs/compute/uploading-large-objects-to-amazon-s3-using-multipart-upload-and-transfer-acceleration/" rel="noopener noreferrer"&gt;JavaScript&lt;br&gt;
SDK&lt;/a&gt;, &lt;br&gt;
который будет обрабатывать всю разбивку на части и загрузку за &lt;br&gt;
вас.&lt;/p&gt;

&lt;p&gt;При загрузке нескольких частей в S3 уведомления о событиях &lt;br&gt;
срабатывают только после завершения всей загрузки (когда все &lt;br&gt;
части собраны), а не при загрузке отдельных частей. Для &lt;br&gt;
отслеживания прогресса загрузки отдельных частей необходимо &lt;br&gt;
использовать API ListParts в S3, который возвращает все &lt;br&gt;
загруженные части с их ETags для текущей загрузки.&lt;/p&gt;

&lt;p&gt;На практике вы будете полагаться на этот API при проектировании &lt;br&gt;
таких систем, как Dropbox. Однако, скорее всего, на собеседовании &lt;br&gt;
вы не сможете просто сказать: "Я бы использовал S3 Multipart &lt;br&gt;
Upload API", не сумев объяснить, как он работает и как бы вы сами &lt;br&gt;
его реализовали, если бы это потребовалось. Тем не менее, &lt;br&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%2Fkq5vpsmvouez5mpzm26w.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%2Fkq5vpsmvouez5mpzm26w.png" alt="Поддержка больших файлов" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Как можно максимально ускорить загрузку, скачивание и синхронизацию данных?
&lt;/h3&gt;

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

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

&lt;p&gt;Однако нам нужно разумно подходить к вопросу сжатия. Сжатие полезно только в том случае, если выигрыш в скорости от передачи меньшего количества байтов перевешивает время, необходимое для сжатия и распаковки файла. Для некоторых типов файлов, особенно медиафайлов, таких как изображения и видео, коэффициент сжатия настолько низок, что время, затрачиваемое на сжатие и распаковку файла, не оправдывает себя. Если вы прямо сейчас возьмете файл &lt;code&gt;.png&lt;/code&gt; и сожмете его, вам повезет, если размер файла уменьшится более чем на несколько процентов - поэтому это не стоит того. С другой стороны, для текстовых файлов коэффициент сжатия намного выше, и в зависимости от условий сети это вполне может быть выгодно. Текстовый файл размером 5 ГБ может быть сжат до 1 ГБ или даже меньше в зависимости от содержимого.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Алгоритмы сжатия&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Существует ряд алгоритмов сжатия файлов. Наиболее &lt;br&gt;
распространенными являются &lt;a href="https://ru.wikipedia.org/wiki/Gzip" rel="noopener noreferrer"&gt;Gzip&lt;/a&gt;, &lt;a href="https://ru.wikipedia.org/wiki/Brotli" rel="noopener noreferrer"&gt;Brotli&lt;/a&gt; и &lt;a href="https://ru.wikipedia.org/wiki/Zstandard" rel="noopener noreferrer"&gt;Zstandard&lt;/a&gt;. Каждый из этих &lt;br&gt;
алгоритмов имеет свои компромиссы с точки зрения степени сжатия и &lt;br&gt;
скорости. Gzip является наиболее распространенным и &lt;br&gt;
поддерживается всеми современными браузерами. Brotli - более &lt;br&gt;
новый алгоритм с более высокой степенью сжатия, чем Gzip, но он &lt;br&gt;
поддерживается не всеми браузерами. Zstandard - самый новый &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;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Как можно обеспечить безопасность файлов?
&lt;/h3&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Шифрование при передаче&lt;/strong&gt;: конечно, для большинства кандидатов это очевидно. Мы должны использовать HTTPS для шифрования данных при их передаче между клиентом и сервером. Это стандартная практика, поддерживаемая всеми современными браузерами.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Шифрование при хранении&lt;/strong&gt;: мы также должны шифровать файлы, когда они хранятся в S3. Это встроенная функция S3, и ее легко включить. Когда файл загружается в S3, мы можем указать, что он должен быть зашифрован. Затем S3 шифрует файл с помощью уникального ключа и сохранит ключ отдельно от файла. Таким образом, даже если кто-то получит доступ к файлу, он не сможет расшифровать его без ключа. Подробнее о шифровании в S3 можно узнать &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html" rel="noopener noreferrer"&gt;здесь&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Контроль доступа&lt;/strong&gt;: наш список общего доступа (&lt;code&gt;sharelist&lt;/code&gt;) или отдельная таблица/кэш общего доступа - это наш базовый ACL (Access Control List, список контроля доступа). Как обсуждалось ранее, мы гарантируем, что предоставляем ссылки для скачивания только авторизованным пользователям.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Здесь снова вступают в игру подписанные URL-адреса, о которых мы говорили ранее. Когда пользователь запрашивает ссылку для скачивания, мы генерируем подписанный URL-адрес, действительный только в течение короткого периода времени (например, 5 минут). Затем этот подписанный URL-адрес отправляется пользователю, который&lt;br&gt;
может использовать его для загрузки файла. Стоит отметить, что подписанные URL-адреса являются токенами "на предъявителя" (bearer token) - любой, у кого есть действительный, непросроченный URL-адрес, может загрузить файл. Короткий срок действия ограничивает уязвимость, но не полностью предотвращает распространение. Для более строгих сценариев безопасности можно добавить дополнительные ограничения, такие как привязка к IP-адресу, или потребовать&lt;br&gt;
использования подписанного URL-адреса в сочетании с аутентификационными файлами cookie.&lt;/p&gt;

&lt;p&gt;Подписанные URL-адреса также работают с современными CDN, такими как CloudFront, и являются функцией S3. Вот как это работает:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Генерация: на сервере генерируется подписанный URL-адрес, включающий подпись, которая обычно содержит путь к URL-адресу, метку времени истечения срока действия и, возможно, другие ограничения (например, IP-адрес). В случае CloudFront эта подпись создается с использованием закрытого ключа поставщика контента.&lt;/li&gt;
&lt;li&gt;Распространение: подписанный URL-адрес распространяется авторизованному пользователю, который может использовать его для прямого доступа к указанному ресурсу из CDN.&lt;/li&gt;
&lt;li&gt;Проверка подписи: когда CDN получает запрос с подписанным URL-адресом, он проверяет подпись, используя соответствующий открытый ключ (зарегистрированный в CloudFront), проверяет метку времени истечения срока действия и любые другие ограничения. Если подпись действительна и срок действия URL-адреса не истек, CDN предоставляет запрошенный контент. В противном случае доступ запрещается.&lt;/li&gt;
&lt;/ol&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 Gateway, ожидайте вопрос "что он делает" и "как работает".&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Задача Dropbox&lt;/strong&gt;: от Middle кандидата ожидается четко определенный API и модель данных, а также высокоуровневый дизайн, который функционально покрывает все процессы загрузки, скачивания и обмена файлами. Не ожидается, что кандидаты сразу будут знать о предварительно подписанных URL-адресах или о прямой загрузке/скачке в/из S3, или сразу предложат разбиение на части. Однако после&lt;br&gt;
уточняющих вопросов, таких как: "Вы сейчас загружаете файл дважды, как этого избежать?" или "Как можно показать прогресс пользователя, позволяя ему возобновить загрузку?", они смогут проанализировать проблему и прийти к решению коммуницируя с интервьюером.&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;: вы должны быть знакомы с современными принципами проектирования систем. Например, знать, как использовать объектное хранилище или как использовать CDN для более быстрой загрузки.&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;Задача Dropbox&lt;/strong&gt;: от Senior кандидатов ожидается, что они быстро пройдут начальный этап проектирования, чтобы затем подробно обсудить, как обрабатывать загрузку больших файлов. Хотя это и не является обязательным требованием, многие кандидаты имеют опыт работы с загрузкой файлов и могут рассказать о некоторых&lt;br&gt;
API (например, о S3 Multipart Upload API) и принципах их работы.&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;Задача Dropbox&lt;/strong&gt;: от Staff+ кандидата ожидается высокое качество решений по сложным задачам, которые обсуждались выше. Сильные кандидаты глубоко разбирают каждую тему, от них также ожидается четкое понимание компромиссов между различными решениями и способность ясно их сформулировать.&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>Thu, 30 Apr 2026 17:01:24 +0000</pubDate>
      <link>https://dev.to/nowinterview/system-design-proiektiruiem-siervis-bystrykh-znakomstv-5fnc</link>
      <guid>https://dev.to/nowinterview/system-design-proiektiruiem-siervis-bystrykh-znakomstv-5fnc</guid>
      <description>&lt;p&gt;Видеоразбор этой задачи &lt;strong&gt;на русском языке&lt;/strong&gt; можно посмотреть здесь - &lt;a href="https://www.youtube.com/watch?v=U_qR7HFZIbE" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=U_qR7HFZIbE&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Больше статей и разборов по System Design: &lt;a href="https://nowinterview.ru" rel="noopener noreferrer"&gt;https://nowinterview.ru&lt;/a&gt;&lt;/p&gt;




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

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

&lt;p&gt;Tinder - это мобильное приложение для знакомств. Пользователи &lt;br&gt;
видят профили друг друга и свайпают (смахивают, swipe) вправо, &lt;br&gt;
если профиль понравился, и влево - если нет. Приложение &lt;br&gt;
использует геоданные и пользовательские фильтры, чтобы показывать &lt;br&gt;
потенциальные матчи (совпадения, match) поблизости.&lt;/p&gt;
&lt;/blockquote&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;Пользователи могут просматривать список потенциальных матчей, соответствующих их предпочтениям и находящихся в пределах максимальной дистанции от текущей локации.&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;Пользователи могут покупать и использовать другие premium функции.&lt;/li&gt;
&lt;/ul&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;h3&gt;
  
  
  Нефункциональные требования
&lt;/h3&gt;

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

&lt;ol&gt;
&lt;li&gt;Система должна обеспечивать сильную согласованность (strong consistency) для свайпов. Если пользователь свайпнул "да" на человека, который уже свайпнул "да" на него, оба должны получить уведомление о матче.&lt;/li&gt;
&lt;li&gt;Система должна масштабироваться под большое количество ежедневных активных / одновременных пользователей (20 млн DAU, в среднем ~100 свайпов на пользователя в день).&lt;/li&gt;
&lt;li&gt;Система должна быстро формировать список потенциальных матчей (&amp;lt; 300 мс).&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;/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%2Fnbhe3jefxf046g78rad9.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%2Fnbhe3jefxf046g78rad9.png" alt="Нефункциональные требования" width="800" height="172"&gt;&lt;/a&gt;&lt;/p&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;Для Tinder основные сущности довольно очевидны:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User&lt;/strong&gt;: это и пользователь приложения, и профиль, который показывают другим пользователям.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swipe&lt;/strong&gt;: выражение "да" или "нет" по отношению к профилю другого пользователя. Содержит информацию о пользователе, который свайпает (&lt;code&gt;swiping_user&lt;/code&gt;), и пользователе, которого свайпают (&lt;code&gt;target_user&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match&lt;/strong&gt;: связь между двумя пользователями, возникающая в результате взаимного свайпа "да".&lt;/li&gt;
&lt;/ol&gt;

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

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

&lt;p&gt;Первый эндпоинт - создание/обновление профиля пользователя. Конечно, в реальном Tinder там будут фото, биография и т. п., но в этой задаче сфокусируемся на предпочтениях для поиска и сопоставления.&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 /profile
{
  min_age,
  max_age,
  max_distance,
  interested_in: "female" | "male",
  ...
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&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;GET /feed?lat={}&amp;amp;long={} -&amp;gt; User[]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;code&gt;GET &lt;br&gt;
/feed&lt;/code&gt;. Для Tinder это обычно избыточно, вместо формирования &lt;br&gt;
страниц, приложение просто вызовет эндпоинт еще раз, если текущий &lt;br&gt;
список потенциальных кандидатов закончился.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&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 /swipe/{userId}
{
  "decision": "yes" | "no"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;В каждом из этих запросов информация о пользователе передается в &lt;br&gt;
заголовках (через session token или auth token). Это &lt;br&gt;
распространенный паттерн, так мы можем обеспечивать &lt;br&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%2Frtejsy81jo3nic1zjrj9.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%2Frtejsy81jo3nic1zjrj9.png" alt="Проектирование API и сущностей" width="784" height="665"&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;code&gt;POST /profile&lt;/code&gt; и сохраняем настройки в базе. Для старта нам достаточно простой архитектуры Клиент -&amp;gt; Сервер -&amp;gt; База данных. При этом, если сразу очевидно, что мы будем использовать несколько сервисов, то можно сразу добавить на схему API Gateway для маршрутизации запросов.&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%2F5rv1xzv676a5wp2nmy3x.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%2F5rv1xzv676a5wp2nmy3x.png" alt="Создание профиля с предпочтениями" width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Клиент&lt;/strong&gt;: пользователи взаимодействуют с системой через мобильное приложение.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&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;/ol&gt;

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

&lt;ol&gt;
&lt;li&gt;Клиент отправляет &lt;code&gt;POST /profile&lt;/code&gt; с данными профиля в теле запроса.&lt;/li&gt;
&lt;li&gt;API Gateway направляет запрос в сервис профилей.&lt;/li&gt;
&lt;li&gt;Сервис профилей обновляет предпочтения пользователя в базе.&lt;/li&gt;
&lt;li&gt;Результат возвращается клиенту.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Когда пользователь открывает приложение, он сразу видит список профилей для свайпа. Эти профили должны соответствовать фильтрам (возраст, интересы и т. п.), а также географии пользователя (например "&amp;lt; 2 км", "&amp;lt; 5 км", "&amp;lt; 15 км").&lt;/p&gt;

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

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

&lt;p&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="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;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;preferredAgeMin&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;preferredAgeMax&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;gender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;preferredInterestedIn&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;userLat&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;preferredDistance&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;userLat&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;preferredDistance&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;long&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;userLong&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;preferredDistance&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;userLong&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;preferredDistance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fmeze69vk0irzbvnieqao.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%2Fmeze69vk0irzbvnieqao.png" alt="Список потенциальных матчей" width="800" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Когда пользователь запрашивает новый набор профилей:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Клиент отправляет &lt;code&gt;GET /feed&lt;/code&gt;, передавая текущую локацию через
query‑параметры.&lt;/li&gt;
&lt;li&gt;API Gateway направляет запрос в сервис профилей.&lt;/li&gt;
&lt;li&gt;Сервис профилей запрашивает базу данных, выбирая пользователей по предпочтениям и локации.&lt;/li&gt;
&lt;li&gt;Результаты возвращаются клиенту.&lt;/li&gt;
&lt;/ol&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;
  
  
  3. Пользователи могут свайпать вправо/влево и выражать да/нет по отношению к другим пользователям
&lt;/h3&gt;

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

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

&lt;p&gt;Добавим два новых компонента:&lt;/p&gt;

&lt;ol&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;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Почему мы выбрали отдельный сервис и отдельное хранилище?&lt;/p&gt;

&lt;p&gt;Обоснование: создание/обновление профиля происходят существенно &lt;br&gt;
реже, чем запись свайпов. Разделив сервисы, мы можем независимо &lt;br&gt;
масштабировать сервис свайпов. Аналогично, по данным, свайпов &lt;br&gt;
будет очень много. При &lt;code&gt;20 млн DAU × 100 свайпов/день × ~100 байт &lt;br&gt;
на свайп&lt;/code&gt; получается порядка &lt;strong&gt;~200GB данных в день&lt;/strong&gt;. Такой &lt;br&gt;
объем и нагрузку хорошо сможет обработать хранилище &lt;br&gt;
оптимизированное для записи такое как Cassandra (которое может &lt;br&gt;
быть не лучшим выбором для профилей). Кроме того, отдельное &lt;br&gt;
хранилище позволяет оптимизировать паттерны доступа и кэширование &lt;br&gt;
для свайпов без влияния на сервис профилей.&lt;/p&gt;

&lt;p&gt;Такое разделение - не универсальный ответ для всех систем. Но &lt;br&gt;
здесь плюсы перевешивают минусы.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Поскольку свайп - действие почти без усилий, можно ожидать большой поток записей. Если принять &lt;code&gt;20 млн DAU&lt;/code&gt; и в среднем &lt;code&gt;100&lt;/code&gt; свайпов/день, это &lt;strong&gt;&lt;code&gt;2 млрд&lt;/code&gt; записей в день&lt;/strong&gt;. Это почти наверняка означает, что данные нужно партиционировать.&lt;/p&gt;

&lt;p&gt;Cassandra хорошо подходит как хранилище свайпов. Мы можем партиционировать по &lt;code&gt;swiping_user_id&lt;/code&gt;. Тогда проверка "свайпал ли пользователь A пользователя B" будет быстрой: мы предсказуемо идем в один раздел (partition). Также Cassandra хорошо выдерживает большие объемы записей благодаря архитектуре своего хранения (CommitLog + Memtables + SSTables). Недостатком использования Cassandra здесь является конечная согласованность для данных о свайпах. Мы обсудим способы нивелировать этот недостаток когда будем погружаться в детали.&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%2Fajyd9lq1jwtmz6vc4hg5.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%2Fajyd9lq1jwtmz6vc4hg5.png" alt="Свайп вправо/влево и запись результата" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Когда пользователь свайпает:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Клиент отправляет &lt;code&gt;POST /swipe/{userId}&lt;/code&gt; с &lt;code&gt;userId&lt;/code&gt; профиля и результатом свайпа (вправо/влево).&lt;/li&gt;
&lt;li&gt;API Gateway направляет запрос в сервис свайпов.&lt;/li&gt;
&lt;li&gt;Сервис свайпов записывает свайп в хранилище свайпов.&lt;/li&gt;
&lt;li&gt;Сервис свайпов проверяет наличие обратного свайпа и, если он есть, сообщает клиенту о матче.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4. Пользователи получают уведомление о матче при взаимном свайпе
&lt;/h3&gt;

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

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

&lt;p&gt;Но что насчет Алисы? Она могла свайпнуть Боба несколькими днями ранее. Нам нужно отправить push‑уведомление на устройство Алисы, сообщив, что у нее новый матч.&lt;/p&gt;

&lt;p&gt;Для этого будем использовать нативные сервисы push-уведомлений, такие как Apple Push Notification Service (APNS) или Firebase Cloud Messaging (FCM).&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%2Ftwbtu9bflsvftwtqe7n3.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%2Ftwbtu9bflsvftwtqe7n3.png" alt="Уведомление о матче" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;APNS и FCM - это службы push‑уведомлений, с собственным набором &lt;br&gt;
API и SDK, которые мы можем использовать для отправки &lt;br&gt;
push‑уведомлений на пользовательские устройства.&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;li&gt;Мы отправляем push‑уведомление через APNS/FCM Алисе, что у нее новый матч.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;h2&gt;
  
  
  Потенциальные погружения в детали
&lt;/h2&gt;

&lt;p&gt;К этому моменту у нас есть базовая работающая система, удовлетворяющая функциональным требованиям. Но есть несколько областей, куда полезно углубиться, чтобы улучшить производительность, масштабируемость и т.д. В зависимости от&lt;br&gt;
вашего уровня, от вас ожидается, что вы будете направлять дискуссию на эти темы, представляющие наибольший интерес.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Как обеспечить согласованность и низкую задержку при свайпах?
&lt;/h3&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;blockquote&gt;
&lt;p&gt;Стоит заметить: можно решить эту проблему и без сильной &lt;br&gt;
согласованности. Например, сделать отдельный процесс согласования &lt;br&gt;
(reconciliation), который периодически пробегает по свайпам и &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;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;В Cassandra есть базовая поддержка того, что они называют легковесные транзакции (Lightweight Transactions, LWT), но это не полноценные ACID‑транзакции. LWT используют консенсусный протокол Paxos для обеспечения линеаризуемой согласованности (linearizable consistency) для конкретных операций, но только в рамках &lt;strong&gt;одного раздела (partition)&lt;/strong&gt;. Между разделами они не дают атомарности,&lt;br&gt;
уровней изоляции и откатов. Также LWT имеют существенные накладные расходы из‑за нескольких сетевых коммуникаций (round-trips) между нодами. Это делает такие транзакции подходящими в основном для простых условных обновлений.&lt;/p&gt;

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

&lt;p&gt;Главная проблема при этом подходе - масштабируемость. &lt;strong&gt;2 млрд свайпов в день&lt;/strong&gt; не поместятся в один раздел, а транзакции на несколько разделов LWT не поддерживают.&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;Можно использовать LWT транзакции Cassandra, атомарно обрабатывая свайпы. Ключевая идея - гарантировать, что все свайпы между двумя пользователями попадают в один раздел.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Сперва создадим таблицу с составным ключом, который группирует пару пользователей:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;swipes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user_pair&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- ключ партиционирования: smaller_id:larger_id&lt;/span&gt;
    &lt;span class="n"&gt;from_user&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;-- кластерный ключ&lt;/span&gt;
    &lt;span class="n"&gt;to_user&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;-- кластерный ключ&lt;/span&gt;
    &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;user_pair&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;from_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;При свайпе формируем &lt;code&gt;user_pair&lt;/code&gt;, отсортировав id, для обеспечения согласованности:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user_pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Отсортируем id так, чтобы (A-&amp;gt;B) и (B-&amp;gt;A) были в одном разделе
&lt;/span&gt;    &lt;span class="n"&gt;sorted_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;user_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_b&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sorted_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sorted_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_swipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user_pair&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_user_pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Обе операции выполняются атомарно в одном разделе
&lt;/span&gt;    &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    BEGIN BATCH
        INSERT INTO swipes (user_pair, from_user, to_user, direction, created_at)
        VALUES (?, ?, ?, ?, ?);

        SELECT direction FROM swipes
        WHERE user_pair = ?
        AND from_user = ?
        AND to_user = ?;
    APPLY BATCH;
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Этот подход эффективен, поскольку транзакции Cassandra в одном разделе обеспечивают необходимые нам гарантии атомарности. Обеспечивая хранение всех операций между двумя пользователями в одном разделе, мы можем атомарно проверять совпадения, что делает решение одновременно производительным и надежным.&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;
  Отличное решение: Redis для атомарных операций
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Ключевая идея та же: свайпы между одной и той же парой пользователей должны попадать на один шард Redis. Можно добиться этого, формируя ключ из двух user_id в детерминированном порядке.&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;Key:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"swipes:123:456"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Value:&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;"123_swipe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"456_swipe"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"no"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Отсортируем id так, чтобы (A-&amp;gt;B) и (B-&amp;gt;A) были в одном разделе
&lt;/span&gt;    &lt;span class="n"&gt;sorted_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;user_a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_b&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;swipes:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sorted_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sorted_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_swipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Используем Redis хеш для хранения свайпов обоих пользователей
&lt;/span&gt;    &lt;span class="c1"&gt;# Хеш имеет два поля: user1_swipe и user2_swipe
&lt;/span&gt;    &lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    redis.call(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HSET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, KEYS[1], ARGV[1], ARGV[2])
    return redis.call(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HGET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, KEYS[1], ARGV[3])
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# Выполняем атомарно используя Lua скрипт
&lt;/span&gt;    &lt;span class="n"&gt;other_swipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;from_user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_swipe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# поле для установки
&lt;/span&gt;            &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;# наш свайп
&lt;/span&gt;            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;to_user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_swipe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# поле для проверки
&lt;/span&gt;        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Возвращает значение другого пользователя
&lt;/span&gt;
    &lt;span class="c1"&gt;# Если другой пользователь тоже свайпнул вправо, это матч!
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;right&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;other_swipe&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;right&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;create_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Используя атомарные операции Redis через Lua скрипт, мы можем гарантировать, что запись свайпа и проверка совпадения выполняются как одна операция. Это дает нам необходимую согласованность и высокую производительность благодаря тому, что Redis работает в памяти. Система масштабируется горизонтально, поскольку мы&lt;br&gt;
можем добавлять больше узлов Redis, а согласованное хеширование гарантирует, что связанные свайпы остаются вместе.&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%2F3ztrghz167efsjf84dvo.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%2F3ztrghz167efsjf84dvo.png" alt="Redis для атомарных операций" width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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



&lt;/p&gt;

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

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

&lt;p&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="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;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;preferredAgeMin&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;preferredAgeMax&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;gender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;preferredInterestedIn&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;userLat&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;preferredDistance&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;userLat&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;preferredDistance&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;long&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="n"&gt;userLong&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;preferredDistance&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;userLong&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;preferredDistance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;
  Хорошее решение: Индексированные БД для real-time запросов
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Один из способов добиться низкой задержки - использовать индексированные БД для запросов реального времени. Если создать индексы по полям, используемым при построении списка (предпочтения, возраст, и особенно geo‑данные), можно сильно ускорить время ответа. Геопространственный индекс позволяет эффективно находить&lt;br&gt;
пользователей в заданной области.&lt;/p&gt;

&lt;p&gt;Для масштабируемости и требований Tinder можно использовать БД оптимизированную для поиска вроде Elasticsearch или OpenSearch. Они заточены под быстрый поиск и сложные запросы, что делает их пригодными для обработки больших объемов данных с минимальной задержкой.&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%2Fhhfim3ax8tokrrrn2630.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%2Fhhfim3ax8tokrrrn2630.png" alt="Индексированные БД для запросов списка потенциальных матчей" width="800" height="525"&gt;&lt;/a&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;Обычно это решают через механизм Change Data Capture (CDC). Это шаблон, который фиксирует изменения базы данных (вставки, обновления, удаления) и передает их в другие системы. Часто реализуется путем мониторинга журнала упреждающей записи&lt;br&gt;
(write-ahead log) базы данных.&lt;/p&gt;

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



&lt;/p&gt;

&lt;p&gt;
  Хорошее решение: Предварительные вычисления и кэширование
  &lt;p&gt;&lt;strong&gt;Подход&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Такой кэшированный список дает пользователю моментальный доступ к профилям, улучшая UX. Предвычисления можно делать в "непиковое" время, а сокращение частоты запуска cron задания помогут держать списки потенциальных матчей актуальными.&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%2F29m5vaz63wfnxp3w1fkt.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%2F29m5vaz63wfnxp3w1fkt.png" alt="Предварительные вычисления и кэширование" width="800" height="503"&gt;&lt;/a&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;



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

&lt;p&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%2Fpasbg6elx1yyu4xdtlaj.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%2Fpasbg6elx1yyu4xdtlaj.png" alt="Комбинация: предварительные вычисления + индексированная база данных" width="800" height="509"&gt;&lt;/a&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;пользователь, предложенный в ленте, может изменить свой профиль (например, интересы) и больше не соответствует критериям фильтра&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Чтобы смягчить эту проблему, можно:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;поставить строгий TTL на кэш списков (например, &amp;lt; 1 часа) и пересчитывать по расписанию&lt;/li&gt;
&lt;li&gt;предвычислять списки только для действительно активных пользователей, а не для всех&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Преимущество заключается в том, что можно тюнить несколько параметров: TTL для кэшированных профилей, количество кэшированных профилей, набор пользователей, для которых мы кэшируем списки, и т. д.&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;p&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;h3&gt;
  
  
  3. Как избежать повторного показа профилей, по которым пользователь уже свайпал?
&lt;/h3&gt;

&lt;p&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;Сервис построения списка может запросить хранилище свайпов и сделать проверку на наличие свайпов пользователя, чтобы отфильтровать профили, по которым уже свайпали. Запрос "все свайпы пользователя" будет эффективным, потому что попадет в один раздел по &lt;code&gt;swiping_user_id&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;С этим подходом есть две проблемы:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Если система предпочитает доступность над согласованностью, часть свайпов могла не попасть на все реплики - мы рискуем "пропустить" свайпы и повторно показать профиль.&lt;/li&gt;
&lt;li&gt;Если у пользователя огромная история свайпов, вернется много id, и проверка на наличие свайпов становится все более дорогой.&lt;/li&gt;
&lt;/ol&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;/p&gt;

&lt;p&gt;Этот клиентский кэш особенно полезен, когда пользователь быстро исчерпывает предвычисленный список. Представьте, что пользователь свайпает 200 заранее подгруженных профилей. Примерно на ~150 профиле клиент может:&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%2Fhhvpf9mk15t39rf59rtl.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%2Fhhvpf9mk15t39rf59rtl.png" alt="Кэш + запрос в хранилище свайпов" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Остается проблема пользователей с очень большой историей свайпов: проверка на &lt;code&gt;contains&lt;/code&gt; по большому множеству &lt;code&gt;id&lt;/code&gt; будет все медленнее по мере роста истории.&lt;/p&gt;



&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Этот подход может выглядеть слегка "over-engineered", но это &lt;br&gt;
вполне разумный сценарий для фильтра Блума: поддержать построение &lt;br&gt;
списка для пользователей с огромной историей свайпов.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Если история свайпов превышает определенный порог (когда проверка на &lt;code&gt;contains&lt;/code&gt; становится дорогой), мы строим и кэшируем фильтр Блума и используем его для фильтрации.&lt;/p&gt;

&lt;p&gt;Фильтр Блума иногда дает ложноположительные результаты (false positives) (например, что профиль уже свайпали, хотя это не так), но &lt;strong&gt;никогда&lt;/strong&gt; не дает ложноотрицательные (false negatives) (например, что профиль не свайпали, если его свайпали). Значит мы точно избегаем повторных показов, но можем не показать пользователю небольшое количество профилей из-за ложноположительных результатов.&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%2Fern7v2y9xkezwjpr97nq.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%2Fern7v2y9xkezwjpr97nq.png" alt="Кэш + запрос в хранилище свайпов + фильтр Блума" width="800" height="555"&gt;&lt;/a&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;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 Gateway, ожидайте вопрос "что он делает" и "как работает".&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Задача Tinder&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;Задача Tinder&lt;/strong&gt;: от Senior кандидата ожидается, что вы быстро пройдете высокоуровневый дизайн и потратите время на детальное обсуждение масштабируемой генерации списка потенциальных матчей и корректного создания матчей. Ожидается, что вы будете проактивно проговаривать компромиссы для построения списка потенциальных матчей, иметь представление о типе индексов которые помогут делать&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;Задача Tinder&lt;/strong&gt;: от Staff+ кандидата ожидается высокое качество решений по сложным задачам, которые обсуждались выше. Сильные кандидаты глубоко разбирают каждую тему, от них также ожидается четкое понимание компромиссов между различными решениями и способность ясно их сформулировать.&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>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;strong&gt;на русском языке&lt;/strong&gt; можно посмотреть здесь - &lt;a href="https://youtu.be/R9B90ewl9EY" rel="noopener noreferrer"&gt;https://youtu.be/R9B90ewl9EY&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Больше статей и разборов по System Design: &lt;a href="https://nowinterview.ru" rel="noopener noreferrer"&gt;https://nowinterview.ru&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;

</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;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;p&gt;Больше статей и разборов по System Design: &lt;a href="https://nowinterview.ru" rel="noopener noreferrer"&gt;https://nowinterview.ru&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;

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