Перейти к содержанию

Основы и подход к задаче

System design на собесе — это не «знаешь ли ты Kafka», а «умеешь ли ты думать по шагам». Здесь мы фиксируем универсальный каркас, по которому ты будешь проходить любую задачу: TinyURL, Twitter, Uber или загадочный «спроектируй нам сервис рекламных кампаний».

Что вообще такое system design

🧩 Простыми словами. Это инженерный планинг до кодинга. Берёшь требования, прикидываешь нагрузку, рисуешь компоненты (API, БД, кэш, очередь), показываешь как они общаются, и обосновываешь почему именно так. Цель — чтобы система делала то, что нужно, выживала под нагрузкой, и переживала отказы.

⚙️ Под капотом. Системный дизайн всегда про trade-offs:

  • Latency vs throughput. Хочешь быстро на запрос или много запросов в сек?
  • Consistency vs availability. Готов ждать пока все реплики синхронизируются?
  • Cost vs reliability. Один датацентр дешевле, два — надёжнее.
  • Complexity vs flexibility. Монолит проще, микросервисы гибче (и больнее).

Хороший ответ всегда явно проговаривает trade-off: «беру eventual consistency, потому что лента — это не банковский баланс, расхождение в 1 секунду нормальное».

💥 Зачем это нужно. В работе — чтобы не переписывать сервис через полгода, когда он не выдержал нагрузку. На собесе — потому что это основной сигнал зрелости инженера.

Каркас ответа: 4 шага

graph LR
    R[1. Уточнить<br/>требования] --> A[2. Дизайн API]
    A --> D[3. Схема данных]
    D --> S[4. Масштаб<br/>и узкие места]
    S -.iterate.-> R

Шаг 1. Уточнить требования (5–10 минут)

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

Functional requirements (что система делает):

  • Какие действия пользователь выполняет? (создать tweet, открыть feed, лайкнуть)
  • Какие гарантии? (доставка сообщения, порядок, дедупликация)
  • Что в скоупе, а что нет? («подписки и DM» — да; «реклама» — нет)

Non-functional requirements (как хорошо она это делает):

  • DAU/MAU — сколько активных пользователей в день/месяц.
  • RPS — сколько запросов в секунду на API. Read RPS и Write RPS отдельно.
  • Latency — p50/p99 каких эндпоинтов. Feed = 200 ms p99, поиск = 500 ms.
  • Consistency — strong или eventual? (см. consistency.md)
  • Availability — 99.9% (8.7 ч даунтайма в год) или 99.99% (52 минуты)?
  • Storage — сколько ГБ/ТБ растёт за год. Считаем users × items × bytes.
  • География — один регион или multi-region.

Capacity estimation на коленке:

100 млн DAU × 5 tweets/сек/user × 280 байт/tweet ≈ ?

Пиши ОЦЕНКУ, не точные цифры. Никто не ждёт от тебя 100% правильного ответа, ждут логику.

Шаг 2. Дизайн API

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

POST /tweets
  Body: { text: string, attachments?: []url }
  Response: { id: int64, created_at: ts }

GET /feed?cursor=...&limit=20
  Response: { items: [...], next_cursor: string }

POST /tweets/:id/likes  (idempotent — повтор не дублирует лайк)

Чек-лист для каждого эндпоинта:

  • HTTP метод (или gRPC unary/stream).
  • Контракт ввода/вывода.
  • Идемпотентность для write-операций (см. resilience.md).
  • Авторизация: кто может звать.
  • Rate limit: есть ли (и каков).

Шаг 3. Схема данных

Здесь решаешь: какие сущности, в какой БД, с какими индексами.

tweets(id PK, user_id, text, created_at, ...)
  index on (user_id, created_at DESC)  -- для профиля
followers(user_id, follower_id)
  index on (user_id), index on (follower_id)
likes(tweet_id, user_id, created_at)  -- дублирующая (tweet+user) PK

Минимально:

  • Что primary key, какие unique constraints.
  • Какие индексы (для каких запросов).
  • Что в SQL, что в NoSQL/KV/blob.
  • Какая денормализация (для скорости — да, для согласованности — больно).

Шаг 4. Масштаб и узкие места

Самая вкусная часть для middle/senior. Разворачиваешь схему в распределённую систему.

graph TB
    User --> CDN[CDN: статика]
    User --> LB[Load Balancer]
    LB --> APIa[API instance 1]
    LB --> APIb[API instance 2]
    APIa --> Cache[(Redis cache)]
    APIa --> DB[(Postgres primary)]
    APIa --> Q[Kafka events]
    DB --> R1[Read replica]
    DB --> R2[Read replica]
    Q --> Worker[Async worker:<br/>fan-out, counters]
    Worker --> DB

Что обязательно проговорить:

  • LB перед API (round-robin, healthcheck).
  • Cache для горячих ключей (Redis cluster, TTL).
  • Read replicas или sharding для роста read RPS.
  • Очередь для асинхронных вещей (counter, fan-out, email).
  • Bottlenecks: где первое узкое место (обычно — write на основной БД).
  • Failure modes: что если упал кэш? что если очередь забилась?

🛠 Применение в Go-проекте

В Go-сервисе живая архитектура примерно такая:

// cmd/api/main.go
func main() {
    cfg := loadConfig()
    db, _ := pgxpool.New(ctx, cfg.PostgresDSN)
    rdb := redis.NewClient(&redis.Options{Addr: cfg.RedisAddr})
    kw := kafka.NewWriter(cfg.KafkaBrokers, "tweet-events")

    h := &Handler{
        repo:  &Repo{db: db},
        cache: &Cache{rdb: rdb, ttl: 5 * time.Minute},
        bus:   &Bus{w: kw},
    }
    srv := &http.Server{
        Addr:              cfg.Addr,
        Handler:           withMiddleware(h.Routes()),
        ReadHeaderTimeout: 5 * time.Second,
    }
    log.Fatal(srv.ListenAndServe())
}

Сюда дальше добавляются: middleware (request_id, metrics, auth), graceful shutdown, circuit breakers на внешние вызовы, tracing. Это и есть «правильный шаблон Go-сервиса в проде».

🔥 Пример сценария: «спроектируй сервис коротких ссылок»

sequenceDiagram
    participant U as User
    participant LB
    participant API
    participant C as Redis
    participant DB as Postgres
    U->>LB: POST /shorten {url}
    LB->>API: forward
    API->>DB: insert + return short_code
    API->>C: SET code → url, TTL 1h
    API-->>U: 201 {short: bm.ru/abc}

    U->>LB: GET /abc
    LB->>API: forward
    API->>C: GET abc
    alt cache hit
        C-->>API: url
    else miss
        API->>DB: select where code=abc
        DB-->>API: url
        API->>C: SET abc → url
    end
    API-->>U: 302 Redirect

Это уже почти полный ответ на собесе, разверни его до уровня подходящего тебе: case-studies.md разбирает TinyURL подробнее.

❌ Типичные ошибки

  • Сразу рисуют схему, не уточнив RPS и storage. → Интервьюер ставит галочку «не задаёт вопросы».
  • Перегружают первую итерацию. Не нужен Kafka на 100 RPS, не нужен sharding на 10 ГБ. Ставь только когда обоснуешь.
  • «Возьмём NoSQL» без объяснения. → Какой именно? Почему не Postgres? Что за query patterns?
  • Игнорят failure modes. Что если упадёт основной мастер? Replica lag — что читатели увидят старые данные?
  • Не считают capacity. «Будет много пользователей». Сколько?
  • Не упоминают monitoring. Без RED-метрик это не production-ready.

🤖 Что спрашивает AI-ментор

  • С чего ты начинаешь ответ на system design?
  • Какие 5 nfr (нефункциональных требований) ты должен уточнить в первые 10 минут?
  • Что входит в capacity estimation для read-heavy сервиса?
  • Зачем рисовать API до схемы данных?
  • Назови три типовых bottleneck'а в монолитном веб-сервисе и как их обходить.

📊 Уровни глубины

L1 (junior). Можешь нарисовать API + Postgres + Redis + LB. Понимаешь, что такое RPS, latency, индекс. Не путаешь cache и persistence.

L2 (middle). Делаешь capacity estimation. Знаешь когда добавить read replicas vs sharding. Различаешь sync vs async вызов. Помнишь про идемпотентность.

L3 (senior). Сначала спрашиваешь про SLA и бюджет. Сравниваешь варианты с trade-offs (sharding by hash vs by range). Знаешь свои failure modes и плановые incidents (replica lag, hot key, schema migration). Можешь сразу сказать «это займёт 3 quarter, упрёмся в команду из 4 человек».

📝 Подумай

  1. Тебе говорят «спроектируй сервис, где пользователи постят короткие видео». Сформулируй 5 уточняющих вопросов в первые 5 минут.
  2. У тебя 50 млн DAU, каждый постит в среднем 1 видео в неделю по 10 МБ. Прикинь storage за год. Где будешь хранить?
  3. Какие три компонента архитектуры обязательно появятся в ответе на любую задачу middle-уровня?
Ответ
  1. (a) read- или write-heavy? (b) сколько RPS на загрузку и просмотр? (c) какой acceptable latency на старт воспроизведения? (d) длина видео и максимальный размер? (e) география — один регион или мульти?
  2. 50e6 × 1/7 × 7 × 52 × 10 МБ ≈ 2.6 ПБ/год. В Postgres такое не положишь — blob storage (S3/MinIO), а в Postgres только метаданные (id, owner, url).
  3. Load balancer + application + database. Дальше — cache + queue, но эти три обязательны.

Дальше: scaling.md — как растягивать сервис под нагрузку.