На собеседовании по system design — проектированию системы — могут попросить придумать сервис коротких ссылок. Недостаточно перечислить базу данных и кэш: нужно уточнить задачу, оценить нагрузку, показать путь запроса и объяснить, что произойдёт при ошибке или росте числа пользователей.

Разберём учебный вариант и сравним его с решениями из материалов о реальных сервисах. Пользователь отправляет длинный адрес и получает короткий; другой человек открывает его и переходит на исходную страницу. Числа, адреса и требования упражнения вымышлены: это не описание сервиса Hintsage.

В документации YOURLS описаны Base36 и Base62, в Shlink есть режимы с учётом регистра и без него, а инженеры Braze рассказывают о коде со случайной составляющей и отметкой времени. Архитектуры отличаются, потому что отличаются и задачи: на интервью полезно показать выбор и его последствия.

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

Шаг 1. Уточните требования

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

После уточнений зафиксируем рамки учебной задачи:

  • Ссылку создаёт пользователь, которого сервис уже умеет распознавать. Открывать короткую ссылку может любой человек без входа.
  • Каждый запрос на создание выдаёт новый код, даже если длинный адрес уже встречался. Пользователь не выбирает код самостоятельно.
  • Ссылка действует 1 095 суток с момента создания. После этого она больше не перенаправляет. Менять адрес назначения или удалять ссылку в первой версии нельзя.
  • Для создания принимаются только корректные абсолютные адреса http и https, длиной до 2 048 байт в UTF-8. Логины и пароли внутри адреса и управляющие символы не принимаются.
  • Аналитика переходов, личный кабинет и выбор собственного короткого адреса в этом упражнении не требуются. Параметры, добавленные к короткому адресу при переходе, не переносим в адрес назначения; параметры, сохранённые в нём при создании, остаются. В других продуктах правила могут отличаться.

Спросим и о цели качества. Допустим, для тестовой короткой ссылки команда хочет за последние 30 дней получать правильный ответ 302 с нужным адресом не менее чем в 99,9% запланированных проверок, каждая — за 200 мс от отправки до получения ответа проверяющей системой. Это цель для последующего измерения, а не доказанная доступность выбранной архитектуры.

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

Шаг 2. Оцените нагрузку и хранение

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

На первые 1 095 суток предположим, что ежедневно создаётся одинаковое число ссылок. Для оценки хранения примем в среднем 1 024 байта на запись вместе с условной долей индекса и служебных данных. Точная величина зависит от длин адресов, структуры базы и настроек; расчёт показывает порядок, а не фактический размер.

Приближённые оценки для учебной нагрузки
Что оцениваемРасчётРезультат
Создание ссылок1 000 000 / 86 400 секунд≈ 12 запросов в секунду в среднем
Переходы100 000 000 / 86 400 секунд≈ 1 157 запросов в секунду в среднем
Пиковые переходыСреднее × 10≈ 11 574 запроса в секунду
Записи за 1 095 дней1 000 000 × 1 095≈ 1,095 млрд записей
Объём одной копии1,095 млрд × 1 024 байта≈ 1,12 ТБ
Две полные копии1,12 ТБ × 2≈ 2,24 ТБ без резервных копий

Для наглядности один терабайт в таблице — 1 000 000 000 000 байт. Две копии — основная база и одна синхронная копия. Они не заменяют резервную копию: она нужна, например, для восстановления после ошибочного удаления данных. Журнал изменений, резервные копии и кэш в полученные 2,24 ТБ не включены.

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

Шаг 3. Опишите API и поведение ошибок

API — договорённость о запросах и ответах между программами. В учебном примере для создания используется POST /api/links. Токен в заголовке подтверждает, кто отправляет запрос. JSON — текстовый формат передачи полей и значений. Адрес s.example.invalid специально выбран как несуществующий учебный домен.

POST /api/links HTTP/1.1
Host: s.example.invalid
Authorization: Bearer TOKEN
Content-Type: application/json

{ "url": "https://example.org/articles/testing?section=api" }

Предположим, запрос обработан 1 ноября 2026 года в 12:00 UTC, а запись получила номер 12 345. Ниже — учебный ответ после сохранения. 201 означает создание, Location указывает на созданную ссылку. Срок действия наступает через 1 095 суток — 31 октября 2029 года в 12:00 UTC.

HTTP/1.1 201 Created
Content-Type: application/json
Location: https://s.example.invalid/3D7
Cache-Control: no-store

{
  "short_url": "https://s.example.invalid/3D7",
  "expires_at": "2029-10-31T12:00:00Z"
}

При открытии GET /3D7 сервис находит запись и отвечает перенаправлением. Заголовок Location содержит исходный адрес; браузер сам выполняет следующий запрос к этому адресу.

HTTP/1.1 302 Found
Location: https://example.org/articles/testing?section=api
Cache-Control: no-store

В нашем контракте 302 означает временное перенаправление. Постоянный 301 тоже встречается в проектах сокращателей, но при истечении ссылки важно отдельно согласовать кэширование ответа. Мы выбираем Cache-Control: no-store, чтобы браузеры и промежуточные HTTP-кэши не сохраняли его и каждое новое обращение доходило до сервиса. Это правило для внешних HTTP-кэшей не запрещает внутренний кэш приложения, который рассмотрим ниже.

Некорректный адрес при создании получает 400, отсутствие действующего входа — 401 с заголовком WWW-Authenticate. Несуществующий, некорректный или просроченный код при открытии получает 404 с Cache-Control: no-store. Если обязательное хранилище недоступно, сервис отвечает 503. Это выбранные правила упражнения; другие продукты могут согласовать иные ответы.

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

Шаг 4. Выберите код и структуру данных

Нам нужно, чтобы два одновременно создаваемых адреса не получили один код. Разделим решение на две части: как получить идентификатор и как записать его в адресе. Для учебной системы выберем последовательность PostgreSQL и представление числа в Base62 — цифрами, заглавными и строчными латинскими буквами. Такой подход разбирают и в примерах проектирования сокращателя ссылок; ниже сравним его с другими вариантами.

Для этого примера порядок символов фиксирован: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz. При таком порядке число 62 записывается как 10, а 12 345 — как 3D7. Обратное преобразование однозначно возвращает номер записи. Заглавные и строчные буквы различаются: 3D7 и 3d7 — разные коды. Порядок алфавита нужно закрепить: например, в документации YOURLS строчные буквы идут перед заглавными, и представление того же числа будет другим.

Перед поиском сервис проверяет код: только эти символы, от 1 до 11 знаков, без начального нуля; полученное число должно быть положительным и помещаться в положительный диапазон типа bigint. Неверный код получает 404. Это исключает разные записи одного и того же числа, например 3D7 и 03D7. При преобразовании нужны целые числа достаточной разрядности: тип, который теряет точность на больших значениях, может превратить разные номера в один.

В таблице достаточно хранить:

  • id — номер записи, первичный ключ таблицы;
  • original_url — проверенный адрес назначения;
  • created_at и expires_at — моменты создания и истечения срока в UTC;
  • owner_id — идентификатор создателя, полученный при проверке входа.

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

При 1,095 млрд новых записей запас шестизначных кодов выглядит достаточным: 62⁶ — около 56,8 млрд возможных сочетаний. Длина нашей записи переменная; пропущенные номера тоже расходуют числовое пространство, поэтому оценка по числу строк приблизительна. Максимальный положительный bigint в Base62 занимает 11 знаков.

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

Что можно использовать вместо Base62

Base62 отвечает на вопрос «как записать значение», но не определяет, откуда оно берётся. Сначала сравним форматы, которыми можно представить идентификатор.

Форматы короткого кода: возможности и ограничения
ФорматСимволы и применениеЧто учесть
Base36Цифры и строчные латинские буквы; подходит для записи числа.Можно договориться не различать регистр; при той же ёмкости код обычно длиннее Base62.
Base62Цифры, строчные и заглавные латинские буквы; подходит для записи числа.Регистр имеет значение, а порядок символов нужно закрепить.
Base64urlБуквы, цифры, дефис и подчёркивание; кодирует последовательность байтов.Правила байтов и заполнения нужно согласовать; регистр значим.
Свой алфавитНабор символов выбирается под ограничения продукта.Меньший алфавит увеличивает длину; совместимость и порядок надо определить заранее.

В Base36 число 12 345 будет 9ix; в нашем Base62 — 3D7. В обоих случаях регистр символов нужно обрабатывать согласно договорённому формату: для Base36 можно заранее привести ввод к нижнему регистру, а Base62 требует различать заглавные и строчные буквы. В Shlink, например, строгий режим различает регистр, а в режиме без учёта регистра используются строчные буквы и цифры.

Base64url кодирует байты, а не число тем же способом, что Base62. Например, если представить 12 345 ровно восьмью байтами в порядке от старшего к младшему и убрать согласованное заполнение =, получится AAAAAAAAMDk. Нельзя сравнивать длину этой строки и 3D7, делая вывод только по размеру алфавита: здесь один формат кодирует фиксированные восемь байт, а другой — число без ведущих нулей. Формат байтов, алфавит и правила заполнения должны быть одинаковы у кодирующей и декодирующей сторон.

Можно выбрать и собственный алфавит, например исключить символы, которые легко перепутать при ручном вводе. Чем меньше символов, тем длиннее код при том же объёме значений. В описании Braze используется Base66 для значения, содержащего случайное число и время создания: это пример выбора под конкретный продукт, а не обязательный стандарт для всех сокращателей.

Как ещё получить уникальный код

Случайная строка. Сервис генерирует символы криптографически стойким генератором случайных чисел и сохраняет код вместе с адресом. Такой код можно составить из символов Base62 или другого подходящего алфавита. При равномерной генерации восьми символов Base62 доступно около 218 трлн сочетаний; однако совпадение возможно задолго до исчерпания пространства. Для миллиона независимо созданных кодов вероятность хотя бы одной коллизии приблизительно 0,23%. На базе ставят уникальное ограничение и при конфликте генерируют новый код: отдельная проверка перед записью не защищает от одновременных запросов. Длину выбирают по ожидаемому числу ссылок и допустимой частоте конфликтов.

Хеш адреса. Можно вычислить, например, SHA-256 от длинного URL и взять часть результата, представив её в выбранном алфавите. Одинаковый вход даст одинаковый хеш, а разные адреса после усечения могут дать один код. Если по условию каждый запрос должен создавать отдельную ссылку, одного URL для генерации недостаточно: нужно добавить различающий запросы компонент и обеспечивать уникальность записи. В отличие от последовательного номера, хеш нельзя использовать для обратного восстановления исходного URL — соответствие всё равно хранится в базе.

Собственный адрес пользователя. Понятный код вроде summer-sale удобен для кампании, но требует правил для длины, допустимых символов, регистра и зарезервированных слов. Занятый код нужно отклонять атомарно при записи. В примере AWS для сокращателя это сделано условной операцией в DynamoDB; в PostgreSQL ту же задачу решает уникальное ограничение. Эта функция потребует расширить наш API и модель данных.

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

Шаг 5. Нарисуйте путь создания и открытия

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

  1. Клиент: отправляет POST и длинный адрес.
  2. Балансировщик: направляет запрос экземпляру сервиса.
  3. Сервис: проверяет пользователя и адрес, получает номер.
  4. PostgreSQL: сохраняет запись и подтверждает транзакцию.
  5. Сервис: возвращает короткий адрес с кодом и сроком действия.
  1. Браузер: запрашивает короткий адрес.
  2. Балансировщик: направляет запрос экземпляру сервиса.
  3. Сервис: проверяет код и ищет запись в Redis.
  4. Если записи нет в кэше: сервис читает основную базу и сохраняет найденную запись в Redis.
  5. Сервис: проверяет срок и возвращает 302 с исходным адресом.

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

На собеседовании можно предложить и другую инфраструктуру: в примере AWS API Gateway обращается непосредственно к DynamoDB, а в описании Braze для повторных чтений используется кэш DAX. Это варианты для других условий и стека, а не подтверждение производительности нашей PostgreSQL-схемы.

Шаг 6. Как кэшировать и не продлевать срок ссылки

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

Пусть обычный срок жизни записи в кэше — не больше пяти минут. Для конкретной ссылки берём меньшую величину: пять минут или время до её истечения. Если осталось меньше секунды, новую запись в кэш не добавляем. Кроме срока в Redis, при каждом чтении сервис сравнивает expires_at со своим текущим временем: если на момент проверки срок уже наступил, он возвращает 404.

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

Допустим, на прогретом кэше 90% переходов находят запись в Redis. При равномерном среднем потоке из 1 157 переходов в секунду примерно 116 запросов в секунду придут в базу; при учебном пике 11 574 — примерно 1 157. Это оценка при том же проценте попаданий, а не гарантия: после очистки кэша или при большом числе новых адресов нагрузка на базу будет выше.

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

Шаг 7. Отказы, безопасность и наблюдение

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

База. Для учебного решения используем основную PostgreSQL и одну синхронную копию в другой зоне. Режим подтверждения записи и порядок переключения нужно настроить и проверить: копия должна содержать подтверждённые записи, а в системе не должно одновременно оказаться двух независимых основных узлов. Во время переключения запросы могут временно завершаться ошибкой. Синхронная запись также добавляет ожидание ответа копии; фактическую задержку нужно измерить.

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

Перебор и вредоносные адреса. Ограничение частоты создания и открытия помогает сдерживать массовые запросы; оно не делает предсказуемые коды секретными. Проверка схемы http/https не доказывает безопасность страницы назначения. В этом упражнении создавать ссылки могут только пользователи, прошедшие вход; порядок выявления и блокировки вредоносных адресов — отдельное требование продукта. Сам сервис перенаправления не загружает целевую страницу: её запрашивает браузер.

Что наблюдать. Отдельно считайте успешные перенаправления, ошибки сервиса и долю отсутствующих ссылок; измеряйте время ответа, попадания в кэш, обращения к базе и задержку записи на синхронную копию. Полезно смотреть не только среднее, но и p95: время, быстрее которого завершились 95% измеренных запросов выбранной группы. Для учебной цели из первого шага нужна отдельная проверка с известной действующей ссылкой и ожидаемым Location.

Следите за ростом задержки и ошибок, загрузкой базы, объёмом кэша и расхождением часов. Затем проводите нагрузочные испытания и учения с отключением узлов. Без них нельзя заключить, что система достигла целевых 99,9% или выдержит рассчитанный пик.

Как объяснить решение на собеседовании

На встрече сначала проговорите задачу и допущения, затем ведите собеседника от самого важного пользовательского пути к деталям. Для нашего примера короткое объяснение может звучать так:

«Сервис создаёт короткие ссылки для вошедших пользователей, а открывать их может любой человек. Я начну с PostgreSQL: её последовательность выдаст номер, который преобразую в Base62. После сохранения верну ссылку. При открытии проверю Redis, а если записи там нет — прочитаю основную базу. Проверю срок действия и верну 302 с исходным адресом. Затем оценю нагрузку на базу при холодном кэше и обсудю отказ Redis и переключение базы».

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

Что проверить перед следующим интервью

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

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