0Pricing
DSA Interview Prep · Урок

Структура собеседования по проектированию систем

Разберите пятишаговую структуру RADIO — Requirements, API, Data, Infrastructure, Optimise — и потренируйтесь применять её к сокращателю URL

«Структура собеседования по проектированию систем» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA Interview Prep содержит 4 уроков всего.

Почему проектирование систем важно на собеседованиях

Собеседования по проектированию систем проверяют вашу способность мыслить в масштабе — как бы вы спроектировали Твиттер, YouTube или сервис сокращения URL для миллиардов пользователей? В отличие от задач на программирование с единственным правильным ответом, проектирование систем не имеет единственного решения: вам нужно принимать компромиссные решения и обосновывать их. На позициях уровня старшего и ведущего специалиста этому этапу посвящают исключительно 30–45 минут.

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

# System design is not about a single correct answer.
# Interviewers look for:
eval_criteria = [
    'Ability to clarify requirements before designing',
    'Back-of-envelope capacity estimation',
    'High-level architecture with clear components',
    'Data modelling and storage choice',
    'Handling scalability (10x, 100x load)',
    'Trade-off discussion (consistency vs availability, etc.)',
    'Communication: talking through decisions as you make them',
]
for c in eval_criteria:
    print('-', c)

Обзор структуры RADIO

Структура RADIO задаёт повторяемую последовательность из пяти шагов для любого собеседования по проектированию систем:

  • R — Требования: функциональные и нефункциональные
  • A — Проектирование API: какие операции предоставляет система?
  • D — Модель данных: какие данные хранятся и каким образом?
  • I — Инфраструктура: компоненты верхнего уровня (серверы, очереди, кэши)
  • O — Оптимизация: узкие места, кэширование, горизонтальное разделение данных, репликация

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

RADIO = {
    'R': 'Requirements — What must the system do? What scale?',
    'A': 'API         — Define endpoints/operations the system exposes',
    'D': 'Data Model  — Entities, schemas, storage types',
    'I': 'Infra       — High-level architecture: servers, queues, caches, CDN',
    'O': 'Optimise    — Identify and address bottlenecks, trade-offs',
}
for step, desc in RADIO.items():
    print(f'[{step}] {desc}')

print('\nTiming guide for a 45-min interview:')
print('  R: 5 min | A: 5 min | D: 10 min | I: 15 min | O: 10 min')

Шаг R: уточнение требований

Никогда не начинайте проектирование, не уточнив требования. Спросите о функциональных требованиях (что делает система) и нефункциональных требованиях (масштаб, задержка, доступность). Для сервиса сокращения URL:

  • Функциональные: сократить URL, перенаправить на исходный URL, при необходимости поддерживать пользовательские псевдонимы и срок действия
  • Нефункциональные: сколько URL создаётся в день? Нагрузка в основном на чтение или на запись? Требование к доступности (99.9% или 99.99%)? Допустимая задержка?

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

# Requirements questions for URL Shortener:
functional = [
    'Shorten a given URL to a 7-character alias',
    'Redirect short URL to original URL',
    'Allow custom aliases (optional)',
    'URL expiry (optional)',
    'Analytics: click count per URL (optional)',
]
non_functional = [
    '100 million new URLs per day (write: ~1160/sec)',
    '10:1 read:write ratio => 11,600 redirects/sec',
    'Redirects must be < 100ms p99 latency',
    '99.99% availability (< 1 hr downtime/year)',
    'URLs must be globally accessible',
]
print('Functional:')
for f in functional: print(' -', f)
print('\nNon-functional:')
for nf in non_functional: print(' -', nf)

Шаг R: предварительная оценка ресурсов

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

Используйте округлённые числа и смело делайте приблизительные оценки. Интервьюеров интересует порядок величины, а не точные значения. Пример для сервиса сокращения URL: 100 млн записей в день ÷ 86400 ≈ 1160 записей/с. 10 млрд чтений в день ÷ 86400 ≈ 115 тыс. чтений/с. Каждая запись URL ≈ 500 байт: 100 млн × 500 байт = 50 GB/день, 18 TB/год.

# Back-of-envelope for URL Shortener
writes_per_day = 100_000_000        # 100 million URLs/day
read_write_ratio = 100              # 100:1 read/write
reads_per_day = writes_per_day * read_write_ratio
bytes_per_url = 500                 # url string + metadata
years_to_store = 5

print('=== Capacity Estimation ===')
print(f'Writes/sec:  {writes_per_day / 86400:.0f}')
print(f'Reads/sec:   {reads_per_day / 86400:,.0f}')
print(f'Storage/day: {writes_per_day * bytes_per_url / 1e9:.1f} GB')
print(f'Storage total ({years_to_store}y): {writes_per_day * bytes_per_url * 365 * years_to_store / 1e12:.1f} TB')

cache_hit_rate = 0.80
hot_urls = reads_per_day * (1 - cache_hit_rate)
print(f'\n80% cache hit rate: {cache_hit_rate*100}% of reads from cache')
print(f'DB reads/sec: {hot_urls / 86400:,.0f}')

Шаг A: проектирование API

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

Для сервиса сокращения URL основными являются два API: (1) POST /shorten для создания короткого URL, (2) GET /{alias} для перенаправления. Дополнительно: DELETE /{alias} для удаления, GET /{alias}/stats для аналитики. Укажите коды ответа (201 Создано, 301 Перенаправление, 404 Не найдено).

# API Design for URL Shortener
apis = [
    {
        'method': 'POST',
        'path': '/api/v1/shorten',
        'request': '{"long_url": "https://...", "alias": "optional", "expires_at": "optional"}',
        'response': '201 Created: {"short_url": "https://short.ly/abc1234", "alias": "abc1234"}',
    },
    {
        'method': 'GET',
        'path': '/{alias}',
        'request': 'No body',
        'response': '301 Redirect to long_url (or 404 Not Found)',
    },
    {
        'method': 'GET',
        'path': '/api/v1/{alias}/stats',
        'request': 'Optional: date range query params',
        'response': '200 OK: {"clicks": 42000, "unique_visitors": 15000}',
    },
]
for api in apis:
    print(f'{api["method"]} {api["path"]}')
    print(f'  Request:  {api["request"]}')
    print(f'  Response: {api["response"]}')
    print()

Шаг D: модель данных

Модель данных определяет, что и как вы храните. Определите основные сущности и их атрибуты. Для сервиса сокращения URL: таблица urls с полями alias (первичный ключ), long_url, created_at, expires_at и user_id. Дополнительно можно использовать таблицу clicks для аналитики.

Крайне важно выбрать подходящий тип хранилища: реляционная БД для структурированных данных со сложными запросами; хранилище «ключ—значение» (Редис, DynamoDB) для поиска псевдонимов за O(1) при большом масштабе; объектное хранилище (S3) для больших двоичных объектов. Для сервиса сокращения URL идеально подходит хранилище «ключ—значение» с ключом в виде псевдонима для чтения, а для записи и управления можно использовать реляционную БД.

# Data model for URL Shortener

# Core table (PostgreSQL)
urls_schema = '''
CREATE TABLE urls (
    alias       VARCHAR(16) PRIMARY KEY,   -- e.g., 'abc1234'
    long_url    TEXT NOT NULL,
    user_id     UUID REFERENCES users(id),
    created_at  TIMESTAMP DEFAULT NOW(),
    expires_at  TIMESTAMP,
    click_count BIGINT DEFAULT 0
);
CREATE INDEX ON urls(user_id);
'''

# Cache layer (Redis) for hot reads
redis_schema = '''
alias  =>  long_url       # O(1) GET on cache hit
TTL = 24 hours (or until expiry)
Cache eviction: LRU
'''

print('PostgreSQL schema:')
print(urls_schema)
print('Redis cache:')
print(redis_schema)
print('Storage split: Redis for hot reads (~80%), PostgreSQL for writes and cold reads')

Шаг I: инфраструктура верхнего уровня

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

  • Балансировщик нагрузки: распределяет трафик между репликами служб записи и чтения
  • Служба записи: генерирует псевдоним, проверяет уникальность, записывает данные в БД, инвалидирует кэш
  • Служба чтения и перенаправления: сначала проверяет кэш Редис, а при промахе обращается к БД
  • Реляционная БД: источник истины (с репликами для чтения)
  • Кластер Редис: кэширует часто запрашиваемые соответствия URL для чтения менее чем за миллисекунду
# ASCII architecture sketch
architecture = '''
         Clients
            |
       [Load Balancer]
       /             \\
  [Write API]    [Read/Redirect API]
      |                  |  |
  [Alias Gen]       [Redis Cache]
      |                  |
  [PostgreSQL]---->[Read Replicas]

Alias Generation:
- Base62 encoding of auto-incrementing ID
- 62^7 = 3.5 trillion unique URLs (enough for 5 years at 100M/day)
- OR random 7-char Base62 with collision check
'''
print(architecture)
print('Key design decisions:')
print('  - Read/Write split: separate services for scalability')
print('  - Cache-aside pattern: read from Redis, fallback to DB')
print('  - 301 vs 302 redirect: 301 cached by browser (less load), 302 always hits server (analytics)')

Шаг O: оптимизация и устранение узких мест

Этап оптимизации устраняет узкие места и позволяет масштабировать систему. Для сервиса сокращения URL основные проблемы таковы: задержка перенаправления (разместите Редис ближе к пользователям, используя CDN или пограничные кэши), уникальность псевдонимов при большом масштабе (используйте централизованный сервер выдачи идентификаторов или хеширование с обнаружением коллизий) и узкое место при записи в БД (пакетные или асинхронные записи через очередь).

Явно обсуждайте компромиссы: перенаправления 301 снижают нагрузку на сервер, но ухудшают точность аналитики; перенаправления 302 можно отслеживать, но они увеличивают задержку. Кэширование с длительным сроком действия снижает нагрузку на БД, но может привести к использованию устаревших URL. Демонстрация понимания этих противоречий говорит о мышлении опытного специалиста.

# Key optimisation discussion points
optimisations = {
    'Read latency': [
        'Redis cluster in multiple regions (CDN-edge caching)',
        '301 redirect for non-tracked URLs (client caches)',
        '302 redirect when click analytics are needed',
    ],
    'Write throughput': [
        'Async writes: accept request, enqueue to Kafka, batch-commit to DB',
        'Ticket server (centralised ID generator) to avoid UUID collision',
        'Or: hash(user_id + timestamp + random) with retry on collision',
    ],
    'Availability': [
        'Multi-AZ PostgreSQL with automatic failover',
        'Redis Sentinel or Redis Cluster for HA cache',
        'Health checks + circuit breaker on each service',
    ],
    'Storage': [
        'Partition urls table by hash(alias) for horizontal scaling',
        'Archive expired URLs to cold storage (S3)',
    ],
}
for area, points in optimisations.items():
    print(f'{area}:')
    for p in points: print(f'  - {p}')
    print()

Генерация псевдонимов: кодирование Base62

Важная техническая деталь сервисов сокращения URL — генерация коротких уникальных псевдонимов. Стандартный подход: взять целочисленный ID с автоинкрементом из базы данных и закодировать его в Base62 (цифры 0–9, буквы a–z и A–Z). Строка Base62 длиной 7 символов может представить 62^7 ≈ 3,5 триллиона уникальных URL — этого достаточно на десятилетия при 100 млн URL в день.

Такой подход не приводит к коллизиям (каждый ID уникален) и создаёт короткие строки, безопасные для URL. Сопоставление ID→псевдоним детерминировано и обратимо. Компромисс заключается в том, что последовательные ID создают предсказуемые псевдонимы (это проблема безопасности). Перемешайте алфавит Base62 или используйте смещение счётчика, чтобы снизить предсказуемость.

import string

BASE62_CHARS = string.digits + string.ascii_lowercase + string.ascii_uppercase
BASE = 62

def encode_base62(num):
    if num == 0:
        return BASE62_CHARS[0]
    result = ''
    while num:
        result = BASE62_CHARS[num % BASE] + result
        num //= BASE
    return result

def decode_base62(s):
    result = 0
    for c in s:
        result = result * BASE + BASE62_CHARS.index(c)
    return result

# Generate aliases for IDs 1, 100, 1000, 10^9
for id_ in [1, 100, 1000, 1_000_000, 10**9, 3_521_614_606207]:
    alias = encode_base62(id_)
    decoded = decode_base62(alias)
    print(f'ID {id_:20,} => alias "{alias}" (len={len(alias)}) => decoded={decoded}')

Применение RADIO при проектировании ленты Твиттера

Кратко применим RADIO к проектированию новостной ленты, подобной ленте Твиттера, чтобы показать универсальность структуры:

  • R: пользователи публикуют записи, подписываются на других пользователей и видят ленту записей тех, на кого подписаны. Масштаб: 300 млн пользователей, 500 млн записей в день, лента должна загружаться за <2 с.
  • A: POST /tweets, GET /feed, GET /timeline/{user_id}
  • D: таблица tweets (id, user_id, content, created_at); таблица follows (follower_id, followee_id); кэш ленты для каждого пользователя
  • I: служба веерного распространения записывает новые записи в ленты подписчиков (предварительно вычисленные); Кассандра для таблиц подписок и записей с преобладанием операций записи; Редис для кэшей лент
# Fan-out on Write vs Fan-out on Read trade-off
fan_out_strategies = {
    'Fan-out on Write (Push)': {
        'How': 'When user A posts, immediately write to all followers feeds',
        'Pro': 'O(1) feed read — feed is precomputed',
        'Con': 'Celebrities with 10M followers => 10M writes per post; very slow write',
        'Best for': 'Users with few followers (regular users)',
    },
    'Fan-out on Read (Pull)': {
        'How': 'When user reads feed, query followees tweets and merge',
        'Pro': 'Writes are fast (one DB write per tweet)',
        'Con': 'Feed read is slow: must query all N followees',
        'Best for': 'Celebrity accounts (few reads per post)',
    },
    'Hybrid': {
        'How': 'Push for regular users, pull for celebrities',
        'Pro': 'Balances read and write cost',
        'Con': 'More complex implementation',
        'Best for': 'Production systems like Twitter',
    },
}
for strategy, details in fan_out_strategies.items():
    print(f'{strategy}:')
    for k, v in details.items(): print(f'  {k}: {v}')
    print()

Распространённые ошибки на собеседованиях по проектированию систем

Избегайте этих распространённых ошибок, из-за которых собеседования по проектированию систем часто идут наперекосяк:

  • Переход к решениям без уточнений: рисование блоков до прояснения требований говорит о слабых инженерных привычках
  • Отсутствие оценок: проектирование без понимания масштаба превращается в гадание
  • Избыточное проектирование: проектирование на 1 миллиард пользователей, когда в условии указано 10 000, тратит время собеседования
  • Отсутствие компромиссов: у каждого выбора есть преимущества и недостатки; если не упоминать их, это говорит о поверхностном понимании
  • Молчание: интервьюерам нужно слышать ход Ваших мыслей; проговаривайте решения по мере их принятия
# Checklist: before you stop talking, verify you covered:
checklist = [
    '[ ] Asked clarifying questions about scale and constraints',
    '[ ] Made capacity estimates (RPS, storage, bandwidth)',
    '[ ] Defined the API surface clearly',
    '[ ] Described the data model and storage choices',
    '[ ] Drew a high-level architecture with named components',
    '[ ] Identified the main bottleneck and proposed a solution',
    '[ ] Discussed at least one trade-off explicitly',
    '[ ] Verified the design meets the stated requirements',
]
for item in checklist:
    print(item)

Быстрая проверка

Проверьте, насколько Вы поняли концепции «Структуры данных и алгоритмы — подготовки к собеседованию по программированию» из этого урока.

Итоги урока

В этом уроке Вы узнали: фреймворк RADIO структурирует собеседования по проектированию систем по разделам «Требования», API, «Модель данных», «Инфраструктура» и «Оптимизация», всегда нужно уточнять функциональные и нефункциональные требования и оценивать ёмкость системы до предложения варианта проектирования, а также явно обсуждать компромиссы — у каждого проектного решения есть преимущества и недостатки, которые интервьюеры ожидают услышать от Вас. Далее мы рассмотрим, как выбирать между SQL- и NoSQL-хранилищами на основе шаблонов доступа, согласованности и масштаба.

Часто задаваемые вопросы

Урок «Структура собеседования по проектированию систем» бесплатный?

Да — полный текст урока «Структура собеседования по проектированию систем» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Структура собеседования по проектированию систем»?

Разберите пятишаговую структуру RADIO — Requirements, API, Data, Infrastructure, Optimise — и потренируйтесь применять её к сокращателю URL Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать DSA Interview Prep?

Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Структура собеседования по проектированию систем»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке DSA Interview Prep?

Да. Каждый урок DSA Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Структура собеседования по проектированию систем
  2. Масштабируемое хранение данных: SQL и NoSQL
  3. Кэширование, CDN и балансировка нагрузки
  4. Проектирование ограничителя частоты и ленты Twitter
← Назад к DSA Interview Prep