System Design интервью появилось потому, что алгоритмические задачи не показывают, как инженер проектирует реальные системы. Можно отлично решать задачи на LeetCode, но не уметь спроектировать масштабируемый API.
Разница между coding и system design
Аспект
Coding Interview
System Design Interview
Фокус
Алгоритм и структуры данных
Архитектура и инфраструктура
Правильных ответов
Обычно один оптимальный
Множество допустимых
Масштаб
Одна функция/класс
Целая система
Оценка
Корректность + сложность
Мышление + trade-offs
Уровень
Junior-Senior
Senior+
Что проверяют на System Design
1. Способность работать с неопределенностью
Реальные задачи всегда неполные. На интервью вам дают размытое условие, и ваша задача -- уточнить требования.
<?php
declare(strict_types=1);
/**
* Interview question: "Design a notification system"
*
* BAD approach: immediately start drawing boxes
*
* GOOD approach: ask clarifying questions first
*/
// Questions to ask:
// 1. What types of notifications? (push, email, SMS, in-app)
// 2. How many users? (1K vs 100M changes everything)
// 3. How many notifications per day?
// 4. Real-time or batched delivery?
// 5. Priority levels? (urgent vs marketing)
// 6. Delivery guarantees? (at-least-once, exactly-once)
// 7. User preferences? (opt-out, channels, schedule)
// After clarifying requirements, define the contract
final readonly class NotificationRequest
{
/**
* @param array<string> $channels Delivery channels
* @param array<string, mixed> $payload Template variables
*/
public function __construct(
public string $userId,
public string $templateId,
public array $channels,
public array $payload,
public Priority $priority = Priority::Normal,
public ?\DateTimeImmutable $scheduledAt = null,
) {}
}
enum Priority: string
{
case Urgent = 'urgent'; // Password reset, security alerts
case High = 'high'; // Order confirmation
case Normal = 'normal'; // Social notifications
case Low = 'low'; // Marketing, digests
}
package notification
import "time"
// Interview question: "Design a notification system"
//
// BAD approach: immediately start drawing boxes
// GOOD approach: ask clarifying questions first
//
// Questions to ask:
// 1. What types of notifications? (push, email, SMS, in-app)
// 2. How many users? (1K vs 100M changes everything)
// 3. How many notifications per day?
// 4. Real-time or batched delivery?
// 5. Priority levels? (urgent vs marketing)
// 6. Delivery guarantees? (at-least-once, exactly-once)
// 7. User preferences? (opt-out, channels, schedule)
// Priority defines notification urgency level.
type Priority string
const (
PriorityUrgent Priority = "urgent" // Password reset, security alerts
PriorityHigh Priority = "high" // Order confirmation
PriorityNormal Priority = "normal" // Social notifications
PriorityLow Priority = "low" // Marketing, digests
)
// NotificationRequest is the contract for sending notifications.
type NotificationRequest struct {
UserID string `json:"user_id"`
TemplateID string `json:"template_id"`
Channels []string `json:"channels"` // Delivery channels
Payload map[string]any `json:"payload"` // Template variables
Priority Priority `json:"priority"`
ScheduledAt *time.Time `json:"scheduled_at,omitempty"`
}
namespace Notification;
// Interview question: "Design a notification system"
//
// BAD approach: immediately start drawing boxes
// GOOD approach: ask clarifying questions first
//
// Questions to ask:
// 1. What types of notifications? (push, email, SMS, in-app)
// 2. How many users? (1K vs 100M changes everything)
// 3. How many notifications per day?
// 4. Real-time or batched delivery?
// 5. Priority levels? (urgent vs marketing)
// 6. Delivery guarantees? (at-least-once, exactly-once)
// 7. User preferences? (opt-out, channels, schedule)
public enum Priority
{
Urgent, // Password reset, security alerts
High, // Order confirmation
Normal, // Social notifications
Low, // Marketing, digests
}
// After clarifying requirements, define the contract.
// A record gives value semantics and non-destructive mutation via `with`.
public sealed record NotificationRequest(
string UserId,
string TemplateId,
IReadOnlyList<string> Channels, // Delivery channels
IReadOnlyDictionary<string, object?> Payload, // Template variables
Priority Priority = Priority.Normal,
DateTimeOffset? ScheduledAt = null);
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any
# Interview question: "Design a notification system"
#
# BAD approach: immediately start drawing boxes
# GOOD approach: ask clarifying questions first
#
# Questions to ask:
# 1. What types of notifications? (push, email, SMS, in-app)
# 2. How many users? (1K vs 100M changes everything)
# 3. How many notifications per day?
# 4. Real-time or batched delivery?
# 5. Priority levels? (urgent vs marketing)
# 6. Delivery guarantees? (at-least-once, exactly-once)
# 7. User preferences? (opt-out, channels, schedule)
class Priority(str, Enum):
URGENT = "urgent" # Password reset, security alerts
HIGH = "high" # Order confirmation
NORMAL = "normal" # Social notifications
LOW = "low" # Marketing, digests
# After clarifying requirements, define the contract.
# frozen=True gives the same immutability as `readonly` in PHP/C#.
@dataclass(frozen=True, slots=True)
class NotificationRequest:
user_id: str
template_id: str
channels: list[str] # Delivery channels
payload: dict[str, Any] # Template variables
priority: Priority = Priority.NORMAL
scheduled_at: datetime | None = None
### 2. Понимание масштабирования
Система для 1000 пользователей и для 100 миллионов -- это совершенно разные системы. Интервьюер хочет увидеть, что вы понимаете, как и когда масштабировать.
<?php
declare(strict_types=1);
/**
* Scale analysis example for notification system
*
* Small scale (1K users):
* - Single server, direct DB writes
* - Synchronous email sending
* - Simple cron for scheduled notifications
*
* Medium scale (1M users):
* - Message queue for async processing
* - Read replicas for user preferences
* - Cache for templates
*
* Large scale (100M users):
* - Partitioned queues by priority
* - Sharded user data
* - Rate limiting per channel
* - Dedicated infrastructure per channel
*/
// Small scale: simple and direct
final class SimpleNotificationSender
{
public function __construct(
private readonly MailerInterface $mailer,
private readonly \PDO $db,
) {}
public function send(string $userId, string $message): void
{
$stmt = $this->db->prepare('SELECT email FROM users WHERE id = :id');
$stmt->execute(['id' => $userId]);
$email = $stmt->fetchColumn();
$this->mailer->send($email, $message);
}
}
// Large scale: queue-based with priority routing
final class ScalableNotificationSender
{
public function __construct(
private readonly MessageProducer $queue,
private readonly UserPreferenceCache $preferences,
private readonly RateLimiter $rateLimiter,
) {}
public function send(NotificationRequest $request): void
{
// Check user preferences before even queuing
$prefs = $this->preferences->get($request->userId);
if (!$prefs->isChannelEnabled($request->channels)) {
return;
}
// Rate limit check per user
if (!$this->rateLimiter->isAllowed($request->userId)) {
// Queue for later delivery
$request = $request->withDelay(seconds: 60);
}
// Route to priority-specific queue
$queueName = match ($request->priority) {
Priority::Urgent => 'notifications.urgent',
Priority::High => 'notifications.high',
Priority::Normal => 'notifications.normal',
Priority::Low => 'notifications.low',
};
$this->queue->publish($queueName, $request);
}
}
package notification
import (
"context"
"database/sql"
"fmt"
)
// Small scale: simple and direct
type SimpleNotificationSender struct {
mailer Mailer
db *sql.DB
}
func (s *SimpleNotificationSender) Send(ctx context.Context, userID, message string) error {
var email string
err := s.db.QueryRowContext(ctx, "SELECT email FROM users WHERE id = $1", userID).Scan(&email)
if err != nil {
return fmt.Errorf("get user email: %w", err)
}
return s.mailer.Send(ctx, email, message)
}
// Large scale: queue-based with priority routing
type ScalableNotificationSender struct {
queue MessageProducer
preferences UserPreferenceCache
rateLimiter RateLimiter
}
func (s *ScalableNotificationSender) Send(ctx context.Context, req *NotificationRequest) error {
// Check user preferences before even queuing
prefs, err := s.preferences.Get(ctx, req.UserID)
if err != nil {
return fmt.Errorf("get preferences: %w", err)
}
if !prefs.IsChannelEnabled(req.Channels) {
return nil
}
// Rate limit check per user
allowed, _ := s.rateLimiter.IsAllowed(ctx, req.UserID)
if !allowed {
req.DelaySeconds = 60
}
// Route to priority-specific queue
var queueName string
switch req.Priority {
case PriorityUrgent:
queueName = "notifications.urgent"
case PriorityHigh:
queueName = "notifications.high"
case PriorityNormal:
queueName = "notifications.normal"
case PriorityLow:
queueName = "notifications.low"
}
return s.queue.Publish(ctx, queueName, req)
}
namespace Notification;
// Scale analysis example for notification system
//
// Small scale (1K users):
// - Single server, direct DB writes
// - Synchronous email sending
// - Simple cron for scheduled notifications
//
// Medium scale (1M users):
// - Message queue for async processing
// - Read replicas for user preferences
// - Cache for templates
//
// Large scale (100M users):
// - Partitioned queues by priority
// - Sharded user data
// - Rate limiting per channel
// - Dedicated infrastructure per channel
// Small scale: simple and direct
public sealed class SimpleNotificationSender(IMailer mailer, IDbConnection db)
{
public async Task SendAsync(string userId, string message, CancellationToken ct)
{
var email = await db.QuerySingleAsync<string>(
"SELECT email FROM users WHERE id = @id", new { id = userId }, ct);
await mailer.SendAsync(email, message, ct);
}
}
// Large scale: queue-based with priority routing
public sealed class ScalableNotificationSender(
IMessageProducer queue,
IUserPreferenceCache preferences,
IRateLimiter rateLimiter)
{
public async Task SendAsync(NotificationRequest request, CancellationToken ct)
{
// Check user preferences before even queuing
var prefs = await preferences.GetAsync(request.UserId, ct);
if (!prefs.IsChannelEnabled(request.Channels))
{
return;
}
// Rate limit check per user
if (!await rateLimiter.IsAllowedAsync(request.UserId, ct))
{
// Queue for later delivery; records copy non-destructively via `with`
request = request with { ScheduledAt = DateTimeOffset.UtcNow.AddSeconds(60) };
}
// Route to priority-specific queue
var queueName = request.Priority switch
{
Priority.Urgent => "notifications.urgent",
Priority.High => "notifications.high",
Priority.Normal => "notifications.normal",
Priority.Low => "notifications.low",
_ => throw new ArgumentOutOfRangeException(nameof(request), request.Priority, "unknown priority"),
};
await queue.PublishAsync(queueName, request, ct);
}
}
from dataclasses import replace
from datetime import datetime, timedelta, timezone
# Scale analysis example for notification system
#
# Small scale (1K users):
# - Single server, direct DB writes
# - Synchronous email sending
# - Simple cron for scheduled notifications
#
# Medium scale (1M users):
# - Message queue for async processing
# - Read replicas for user preferences
# - Cache for templates
#
# Large scale (100M users):
# - Partitioned queues by priority
# - Sharded user data
# - Rate limiting per channel
# - Dedicated infrastructure per channel
# Small scale: simple and direct
class SimpleNotificationSender:
def __init__(self, mailer: Mailer, pool: ConnectionPool) -> None:
self._mailer = mailer
self._pool = pool
async def send(self, user_id: str, message: str) -> None:
async with self._pool.acquire() as conn:
email = await conn.fetchval(
"SELECT email FROM users WHERE id = $1", user_id
)
await self._mailer.send(email, message)
# Large scale: queue-based with priority routing
class ScalableNotificationSender:
_QUEUES: dict[Priority, str] = {
Priority.URGENT: "notifications.urgent",
Priority.HIGH: "notifications.high",
Priority.NORMAL: "notifications.normal",
Priority.LOW: "notifications.low",
}
def __init__(
self,
queue: MessageProducer,
preferences: UserPreferenceCache,
rate_limiter: RateLimiter,
) -> None:
self._queue = queue
self._preferences = preferences
self._rate_limiter = rate_limiter
async def send(self, request: NotificationRequest) -> None:
# Check user preferences before even queuing
prefs = await self._preferences.get(request.user_id)
if not prefs.is_channel_enabled(request.channels):
return
# Rate limit check per user
if not await self._rate_limiter.is_allowed(request.user_id):
# Queue for later delivery; the request is frozen, so copy it
request = replace(
request,
scheduled_at=datetime.now(timezone.utc) + timedelta(seconds=60),
)
# Route to priority-specific queue
try:
queue_name = self._QUEUES[request.priority]
except KeyError as exc:
raise ValueError(f"unknown priority: {request.priority}") from exc
await self._queue.publish(queue_name, request)
### 3. Знание trade-offs
Каждое архитектурное решение -- это компромисс. Интервьюер ищет способность осознанно выбирать и обосновывать выбор.
Типичные trade-offs:
Решение A
Решение B
Trade-off
SQL (PostgreSQL)
NoSQL (MongoDB)
Консистентность vs гибкость схемы
Синхронная обработка
Очередь сообщений
Простота vs масштабируемость
Монолит
Микросервисы
Скорость разработки vs независимость деплоя
Push-модель
Pull-модель
Актуальность vs нагрузка на сервер
Кэширование
Прямые запросы
Скорость vs свежесть данных
4. Навыки коммуникации
System Design интервью -- это диалог, а не монолог. Интервьюер оценивает:
Структурированность -- есть ли у вас план, или вы мечетесь между темами
Ясность -- можете ли вы объяснить сложные концепции простыми словами
Восприимчивость -- слышите ли вы подсказки интервьюера
Обоснованность -- можете ли вы аргументировать свои решения
5. Практический опыт
Интервьюер различает теоретические знания и реальный опыт. Кандидат с опытом:
Упоминает конкретные проблемы, с которыми сталкивался
<?php
declare(strict_types=1);
/**
* System Design evaluation matrix
* Typical scoring rubric used by interviewers
*/
final class InterviewEvaluation
{
// Scored from 1 (poor) to 5 (exceptional)
/** Requirements gathering and scoping */
public int $problemExploration;
/** Quality of high-level architecture */
public int $highLevelDesign;
/** Depth in critical components */
public int $detailedDesign;
/** Understanding of trade-offs */
public int $tradeoffAnalysis;
/** Knowledge of scaling patterns */
public int $scalability;
/** Communication and structure */
public int $communication;
/**
* Scoring guide:
*
* 1 - Cannot discuss the topic
* 2 - Surface-level understanding, major gaps
* 3 - Solid understanding, some gaps
* 4 - Deep understanding, discusses alternatives
* 5 - Expert level, anticipates issues proactively
*/
public function getOverallScore(): float
{
$weights = [
'problemExploration' => 0.15,
'highLevelDesign' => 0.20,
'detailedDesign' => 0.25,
'tradeoffAnalysis' => 0.20,
'scalability' => 0.10,
'communication' => 0.10,
];
$total = 0;
foreach ($weights as $field => $weight) {
$total += $this->{$field} * $weight;
}
return round($total, 2);
}
}
package interview
import "math"
// InterviewEvaluation represents the scoring rubric used by interviewers.
// Each field is scored from 1 (poor) to 5 (exceptional).
//
// Scoring guide:
// 1 - Cannot discuss the topic
// 2 - Surface-level understanding, major gaps
// 3 - Solid understanding, some gaps
// 4 - Deep understanding, discusses alternatives
// 5 - Expert level, anticipates issues proactively
type InterviewEvaluation struct {
ProblemExploration int // Requirements gathering and scoping
HighLevelDesign int // Quality of high-level architecture
DetailedDesign int // Depth in critical components
TradeoffAnalysis int // Understanding of trade-offs
Scalability int // Knowledge of scaling patterns
Communication int // Communication and structure
}
func (e *InterviewEvaluation) OverallScore() float64 {
type weight struct {
score int
factor float64
}
weights := []weight{
{e.ProblemExploration, 0.15},
{e.HighLevelDesign, 0.20},
{e.DetailedDesign, 0.25},
{e.TradeoffAnalysis, 0.20},
{e.Scalability, 0.10},
{e.Communication, 0.10},
}
var total float64
for _, w := range weights {
total += float64(w.score) * w.factor
}
return math.Round(total*100) / 100
}
namespace Interview;
// System Design evaluation matrix
// Typical scoring rubric used by interviewers.
// Each score runs from 1 (poor) to 5 (exceptional).
//
// Scoring guide:
// 1 - Cannot discuss the topic
// 2 - Surface-level understanding, major gaps
// 3 - Solid understanding, some gaps
// 4 - Deep understanding, discusses alternatives
// 5 - Expert level, anticipates issues proactively
public sealed record InterviewEvaluation(
int ProblemExploration, // Requirements gathering and scoping
int HighLevelDesign, // Quality of high-level architecture
int DetailedDesign, // Depth in critical components
int TradeoffAnalysis, // Understanding of trade-offs
int Scalability, // Knowledge of scaling patterns
int Communication) // Communication and structure
{
public double OverallScore()
{
(int Score, double Weight)[] weights =
[
(ProblemExploration, 0.15),
(HighLevelDesign, 0.20),
(DetailedDesign, 0.25),
(TradeoffAnalysis, 0.20),
(Scalability, 0.10),
(Communication, 0.10),
];
var total = weights.Sum(w => w.Score * w.Weight);
return Math.Round(total, 2);
}
}
from dataclasses import dataclass
# System Design evaluation matrix
# Typical scoring rubric used by interviewers.
# Each score runs from 1 (poor) to 5 (exceptional).
#
# Scoring guide:
# 1 - Cannot discuss the topic
# 2 - Surface-level understanding, major gaps
# 3 - Solid understanding, some gaps
# 4 - Deep understanding, discusses alternatives
# 5 - Expert level, anticipates issues proactively
@dataclass(frozen=True, slots=True)
class InterviewEvaluation:
problem_exploration: int # Requirements gathering and scoping
high_level_design: int # Quality of high-level architecture
detailed_design: int # Depth in critical components
tradeoff_analysis: int # Understanding of trade-offs
scalability: int # Knowledge of scaling patterns
communication: int # Communication and structure
def overall_score(self) -> float:
weights = (
(self.problem_exploration, 0.15),
(self.high_level_design, 0.20),
(self.detailed_design, 0.25),
(self.tradeoff_analysis, 0.20),
(self.scalability, 0.10),
(self.communication, 0.10),
)
total = sum(score * weight for score, weight in weights)
return round(total, 2)
## Распространенные ошибки
1. Прыгать к решению без уточнения требований
Это самая частая ошибка. Кандидат сразу начинает рисовать диаграммы, не спросив ни одного вопроса.
2. Фокусироваться только на happy path
Реальные системы ломаются. Интервьюер хочет услышать:
Что происходит при сбое компонента?
Как обрабатываются дубликаты?
Что если данные повреждены?
3. Использовать модные слова без понимания
"Давайте добавим Kafka" -- а зачем? "Нам нужен Kubernetes" -- для 100 запросов в минуту? Каждая технология должна быть обоснована.
4. Игнорировать нефункциональные требования
Задержка (latency)
Доступность (availability)
Пропускная способность (throughput)
Стоимость (cost)
Операционная сложность (operational complexity)
5. Не управлять временем
45 минут пролетают быстро. Если потратить 20 минут на один компонент, на остальные не хватит времени.
Как System Design отличается от реальной работы
Интервью
Реальная работа
45 минут
Недели/месяцы
Один человек
Команда
Whiteboard/бумага
Код + инфраструктура
Широкий обзор
Глубокая детализация
Устная коммуникация
Документация + код
Нет legacy
Много legacy
Ключевое: На интервью оценивается ваше мышление и подход, а не идеальный финальный результат. Процесс важнее ответа.
Выводы
System Design интервью -- это проверка вашей способности проектировать реальные системы. Оно оценивает не запоминание паттернов, а умение думать, коммуницировать и принимать обоснованные решения в условиях неопределенности.
Подготовка к System Design -- это не заучивание, а развитие инженерного мышления. Это навык, который полезен далеко за пределами интервью.