Долгосрочная подготовка -- это не заучивание решений, а развитие инженерного мышления. Цель -- научиться проектировать системы, а не запомнить шаблоны для конкретных задач.
Три столпа подготовки
Столп
Описание
Доля времени
Теория
Фундаментальные концепции
30%
Практика
Решение задач, mock interviews
40%
Опыт
Работа с реальными системами
30%
План на 6 месяцев
Месяц 1-2: Фундамент
Цель: Освоить базовые концепции распределенных систем.
Темы для изучения:
Сетевые протоколы (HTTP, TCP, WebSocket)
Базы данных (SQL, NoSQL, когда что)
Кэширование (уровни, стратегии инвалидации)
Балансировка нагрузки (алгоритмы)
Очереди сообщений (RabbitMQ, Kafka -- концепции)
<?php
declare(strict_types=1);
/**
* Month 1-2: Build foundation by implementing core patterns
*
* Exercise: Implement a simple in-memory cache with TTL
* This teaches: cache eviction, time management, memory limits
*/
final class SimpleCache
{
/** @var array<string, CacheEntry> */
private array $entries = [];
private int $maxSize;
public function __construct(int $maxSize = 1000)
{
$this->maxSize = $maxSize;
}
public function get(string $key): mixed
{
if (!isset($this->entries[$key])) {
return null;
}
$entry = $this->entries[$key];
// Check TTL
if ($entry->isExpired()) {
unset($this->entries[$key]);
return null;
}
// Update access time for LRU
$entry->lastAccessed = time();
return $entry->value;
}
public function set(string $key, mixed $value, int $ttlSeconds = 3600): void
{
// Evict if at capacity
if (count($this->entries) >= $this->maxSize && !isset($this->entries[$key])) {
$this->evictLRU();
}
$this->entries[$key] = new CacheEntry(
value: $value,
expiresAt: time() + $ttlSeconds,
lastAccessed: time(),
);
}
/**
* LRU eviction: remove the least recently used entry.
* In production, use a doubly-linked list for O(1) eviction.
* Here simplified for learning purposes.
*/
private function evictLRU(): void
{
$oldestKey = null;
$oldestTime = PHP_INT_MAX;
foreach ($this->entries as $key => $entry) {
if ($entry->lastAccessed < $oldestTime) {
$oldestTime = $entry->lastAccessed;
$oldestKey = $key;
}
}
if ($oldestKey !== null) {
unset($this->entries[$oldestKey]);
}
}
}
final class CacheEntry
{
public function __construct(
public readonly mixed $value,
public readonly int $expiresAt,
public int $lastAccessed,
) {}
public function isExpired(): bool
{
return time() > $this->expiresAt;
}
}
package interview
import (
"math"
"sync"
"time"
)
// SimpleCache is an in-memory cache with TTL and LRU eviction.
// Exercise: teaches cache eviction, time management, memory limits.
type SimpleCache struct {
mu sync.Mutex
entries map[string]*CacheEntry
maxSize int
}
type CacheEntry struct {
Value any
ExpiresAt time.Time
LastAccessed time.Time
}
func NewSimpleCache(maxSize int) *SimpleCache {
return &SimpleCache{
entries: make(map[string]*CacheEntry),
maxSize: maxSize,
}
}
func (c *SimpleCache) Get(key string) (any, bool) {
c.mu.Lock()
defer c.mu.Unlock()
entry, ok := c.entries[key]
if !ok {
return nil, false
}
// Check TTL
if time.Now().After(entry.ExpiresAt) {
delete(c.entries, key)
return nil, false
}
// Update access time for LRU
entry.LastAccessed = time.Now()
return entry.Value, true
}
func (c *SimpleCache) Set(key string, value any, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
// Evict if at capacity
if len(c.entries) >= c.maxSize {
if _, exists := c.entries[key]; !exists {
c.evictLRU()
}
}
c.entries[key] = &CacheEntry{
Value: value,
ExpiresAt: time.Now().Add(ttl),
LastAccessed: time.Now(),
}
}
// evictLRU removes the least recently used entry.
// In production, use a doubly-linked list for O(1) eviction.
func (c *SimpleCache) evictLRU() {
var oldestKey string
oldestTime := time.Unix(math.MaxInt64, 0)
for key, entry := range c.entries {
if entry.LastAccessed.Before(oldestTime) {
oldestTime = entry.LastAccessed
oldestKey = key
}
}
if oldestKey != "" {
delete(c.entries, oldestKey)
}
}
namespace Interview;
/// <summary>
/// In-memory cache with TTL and LRU eviction.
/// Exercise: teaches cache eviction, time management, memory limits.
/// </summary>
public sealed class SimpleCache
{
private readonly Dictionary<string, CacheEntry> _entries = [];
private readonly object _gate = new();
private readonly int _maxSize;
public SimpleCache(int maxSize = 1000) => _maxSize = maxSize;
public bool TryGet(string key, out object? value)
{
lock (_gate)
{
value = null;
if (!_entries.TryGetValue(key, out var entry))
{
return false;
}
// Check TTL
if (entry.IsExpired)
{
_entries.Remove(key);
return false;
}
// Update access time for LRU
entry.LastAccessed = DateTimeOffset.UtcNow;
value = entry.Value;
return true;
}
}
public void Set(string key, object? value, TimeSpan ttl)
{
lock (_gate)
{
// Evict if at capacity
if (_entries.Count >= _maxSize && !_entries.ContainsKey(key))
{
EvictLru();
}
_entries[key] = new CacheEntry(value, DateTimeOffset.UtcNow.Add(ttl))
{
LastAccessed = DateTimeOffset.UtcNow,
};
}
}
/// <summary>
/// Removes the least recently used entry.
/// In production, use a linked list for O(1) eviction.
/// </summary>
private void EvictLru()
{
string? oldestKey = null;
var oldestTime = DateTimeOffset.MaxValue;
foreach (var (key, entry) in _entries)
{
if (entry.LastAccessed < oldestTime)
{
oldestTime = entry.LastAccessed;
oldestKey = key;
}
}
if (oldestKey is not null)
{
_entries.Remove(oldestKey);
}
}
}
public sealed class CacheEntry(object? value, DateTimeOffset expiresAt)
{
public object? Value { get; } = value;
public DateTimeOffset ExpiresAt { get; } = expiresAt;
public required DateTimeOffset LastAccessed { get; set; }
public bool IsExpired => DateTimeOffset.UtcNow > ExpiresAt;
}
import threading
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
@dataclass
class CacheEntry:
value: object
expires_at: datetime
last_accessed: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@property
def is_expired(self) -> bool:
return datetime.now(timezone.utc) > self.expires_at
class SimpleCache:
"""In-memory cache with TTL and LRU eviction.
Exercise: teaches cache eviction, time management, memory limits.
"""
def __init__(self, max_size: int = 1000) -> None:
self._entries: dict[str, CacheEntry] = {}
self._lock = threading.Lock()
self._max_size = max_size
def get(self, key: str) -> object | None:
with self._lock:
entry = self._entries.get(key)
if entry is None:
return None
# Check TTL
if entry.is_expired:
del self._entries[key]
return None
# Update access time for LRU
entry.last_accessed = datetime.now(timezone.utc)
return entry.value
def set(self, key: str, value: object, ttl: timedelta) -> None:
with self._lock:
# Evict if at capacity
if len(self._entries) >= self._max_size and key not in self._entries:
self._evict_lru()
self._entries[key] = CacheEntry(
value=value,
expires_at=datetime.now(timezone.utc) + ttl,
)
def _evict_lru(self) -> None:
"""Remove the least recently used entry.
In production, use collections.OrderedDict for O(1) eviction.
"""
if not self._entries:
return
oldest_key = min(self._entries, key=lambda k: self._entries[k].last_accessed)
del self._entries[oldest_key]
### Месяц 3-4: Углубление
Цель: Изучить продвинутые паттерны и решить 10-15 задач.
Темы:
Шардирование и репликация
Консистентность (CAP, eventual consistency)
Event-driven архитектура
Микросервисы vs монолит
CDN и географическое распределение
Практика:
Решайте по 2-3 задачи в неделю. Формат:
Таймер на 45 минут
Решаете вслух (записывайте голос или видео)
Сравниваете с эталонным решением
Записываете, что упустили
<?php
declare(strict_types=1);
/**
* Month 3-4: Practice designing components
*
* Exercise: Implement consistent hashing
* This teaches: data distribution, rebalancing, virtual nodes
*/
final class ConsistentHashRing
{
/** @var array<int, string> hash => node */
private array $ring = [];
/** @var array<int> sorted hashes */
private array $sortedHashes = [];
public function __construct(
private readonly int $virtualNodes = 150,
) {}
public function addNode(string $node): void
{
for ($i = 0; $i < $this->virtualNodes; $i++) {
$hash = $this->hash(sprintf('%s:%d', $node, $i));
$this->ring[$hash] = $node;
$this->sortedHashes[] = $hash;
}
sort($this->sortedHashes);
}
public function removeNode(string $node): void
{
for ($i = 0; $i < $this->virtualNodes; $i++) {
$hash = $this->hash(sprintf('%s:%d', $node, $i));
unset($this->ring[$hash]);
}
$this->sortedHashes = array_values(array_filter(
$this->sortedHashes,
fn(int $h) => isset($this->ring[$h]),
));
}
/**
* Get the node responsible for a given key.
* Finds the first node clockwise from the key's hash.
*/
public function getNode(string $key): ?string
{
if (empty($this->sortedHashes)) {
return null;
}
$hash = $this->hash($key);
// Binary search for the first hash >= key hash
foreach ($this->sortedHashes as $nodeHash) {
if ($nodeHash >= $hash) {
return $this->ring[$nodeHash];
}
}
// Wrap around to the first node
return $this->ring[$this->sortedHashes[0]];
}
private function hash(string $key): int
{
return crc32($key);
}
}
package interview
import (
"fmt"
"hash/crc32"
"sort"
)
// ConsistentHashRing implements consistent hashing with virtual nodes.
// Exercise: teaches data distribution, rebalancing, virtual nodes.
type ConsistentHashRing struct {
ring map[uint32]string
sortedHashes []uint32
virtualNodes int
}
func NewConsistentHashRing(virtualNodes int) *ConsistentHashRing {
return &ConsistentHashRing{
ring: make(map[uint32]string),
virtualNodes: virtualNodes,
}
}
func (r *ConsistentHashRing) AddNode(node string) {
for i := range r.virtualNodes {
h := r.hash(fmt.Sprintf("%s:%d", node, i))
r.ring[h] = node
r.sortedHashes = append(r.sortedHashes, h)
}
sort.Slice(r.sortedHashes, func(i, j int) bool {
return r.sortedHashes[i] < r.sortedHashes[j]
})
}
func (r *ConsistentHashRing) RemoveNode(node string) {
for i := range r.virtualNodes {
h := r.hash(fmt.Sprintf("%s:%d", node, i))
delete(r.ring, h)
}
filtered := r.sortedHashes[:0]
for _, h := range r.sortedHashes {
if _, ok := r.ring[h]; ok {
filtered = append(filtered, h)
}
}
r.sortedHashes = filtered
}
// GetNode finds the first node clockwise from the key's hash.
func (r *ConsistentHashRing) GetNode(key string) (string, bool) {
if len(r.sortedHashes) == 0 {
return "", false
}
h := r.hash(key)
// Binary search for the first hash >= key hash
idx := sort.Search(len(r.sortedHashes), func(i int) bool {
return r.sortedHashes[i] >= h
})
// Wrap around to the first node
if idx >= len(r.sortedHashes) {
idx = 0
}
return r.ring[r.sortedHashes[idx]], true
}
func (r *ConsistentHashRing) hash(key string) uint32 {
return crc32.ChecksumIEEE([]byte(key))
}
using System.IO.Hashing;
using System.Text;
namespace Interview;
/// <summary>
/// Consistent hashing with virtual nodes.
/// Exercise: teaches data distribution, rebalancing, virtual nodes.
/// </summary>
public sealed class ConsistentHashRing
{
private readonly Dictionary<uint, string> _ring = [];
private readonly List<uint> _sortedHashes = [];
private readonly int _virtualNodes;
public ConsistentHashRing(int virtualNodes = 150) => _virtualNodes = virtualNodes;
public void AddNode(string node)
{
for (var i = 0; i < _virtualNodes; i++)
{
var h = Hash($"{node}:{i}");
_ring[h] = node;
_sortedHashes.Add(h);
}
_sortedHashes.Sort();
}
public void RemoveNode(string node)
{
for (var i = 0; i < _virtualNodes; i++)
{
_ring.Remove(Hash($"{node}:{i}"));
}
_sortedHashes.RemoveAll(h => !_ring.ContainsKey(h));
}
/// <summary>
/// Finds the first node clockwise from the key's hash.
/// </summary>
public string? GetNode(string key)
{
if (_sortedHashes.Count == 0)
{
return null;
}
// BinarySearch returns the bitwise complement of the insertion point
// when there is no exact match — that index is the first hash >= key hash.
var idx = _sortedHashes.BinarySearch(Hash(key));
if (idx < 0)
{
idx = ~idx;
}
// Wrap around to the first node
if (idx >= _sortedHashes.Count)
{
idx = 0;
}
return _ring[_sortedHashes[idx]];
}
private static uint Hash(string key) => Crc32.HashToUInt32(Encoding.UTF8.GetBytes(key));
}
import bisect
from zlib import crc32
class ConsistentHashRing:
"""Consistent hashing with virtual nodes.
Exercise: teaches data distribution, rebalancing, virtual nodes.
"""
def __init__(self, virtual_nodes: int = 150) -> None:
self._ring: dict[int, str] = {}
self._sorted_hashes: list[int] = []
self._virtual_nodes = virtual_nodes
def add_node(self, node: str) -> None:
for i in range(self._virtual_nodes):
h = self._hash(f"{node}:{i}")
self._ring[h] = node
self._sorted_hashes.append(h)
self._sorted_hashes.sort()
def remove_node(self, node: str) -> None:
for i in range(self._virtual_nodes):
self._ring.pop(self._hash(f"{node}:{i}"), None)
self._sorted_hashes = [h for h in self._sorted_hashes if h in self._ring]
def get_node(self, key: str) -> str | None:
"""Find the first node clockwise from the key's hash."""
if not self._sorted_hashes:
return None
# Binary search for the first hash >= key hash
idx = bisect.bisect_left(self._sorted_hashes, self._hash(key))
# Wrap around to the first node
if idx >= len(self._sorted_hashes):
idx = 0
return self._ring[self._sorted_hashes[idx]]
@staticmethod
def _hash(key: str) -> int:
return crc32(key.encode())
### Месяц 5-6: Продвинутый уровень
Цель: Освоить сложные сценарии и научиться уверенно вести интервью.
<?php
declare(strict_types=1);
/**
* Month 5-6: Advanced patterns
*
* Exercise: Implement distributed rate limiter using Redis
* with sliding window algorithm
*/
final class DistributedRateLimiter
{
public function __construct(
private readonly \Redis $redis,
private readonly int $maxRequests,
private readonly int $windowSeconds,
) {}
/**
* Sliding window counter using Redis sorted sets.
*
* Algorithm:
* 1. Remove expired entries (older than window)
* 2. Count current entries
* 3. If under limit, add new entry
* 4. All in a single pipeline (atomic-ish)
*/
public function attempt(string $identifier): RateLimitResult
{
$key = sprintf('ratelimit:%s', $identifier);
$now = microtime(true);
$windowStart = $now - $this->windowSeconds;
// Lua script for atomic check-and-add
$script = <<<'LUA'
local key = KEYS[1]
local window_start = tonumber(ARGV[1])
local now = tonumber(ARGV[2])
local max_requests = tonumber(ARGV[3])
local window = tonumber(ARGV[4])
-- Remove expired entries
redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start)
-- Count current entries
local current = redis.call('ZCARD', key)
if current < max_requests then
-- Add new entry
redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
redis.call('EXPIRE', key, window)
return {1, max_requests - current - 1}
else
return {0, 0}
end
LUA;
$result = $this->redis->eval(
$script,
[$key, (string) $windowStart, (string) $now, $this->maxRequests, $this->windowSeconds],
1,
);
return new RateLimitResult(
allowed: (bool) $result[0],
remaining: (int) $result[1],
resetAt: (int) ceil($now) + $this->windowSeconds,
);
}
}
final readonly class RateLimitResult
{
public function __construct(
public bool $allowed,
public int $remaining,
public int $resetAt,
) {}
}
package interview
import (
"context"
"fmt"
"math"
"time"
"github.com/redis/go-redis/v9"
)
// DistributedRateLimiter uses Redis sorted sets for sliding window rate limiting.
type DistributedRateLimiter struct {
rdb *redis.Client
maxRequests int
windowSeconds int
}
// RateLimitResult holds the result of a rate limit check.
type RateLimitResult struct {
Allowed bool
Remaining int
ResetAt int64
}
func NewDistributedRateLimiter(rdb *redis.Client, maxRequests, windowSeconds int) *DistributedRateLimiter {
return &DistributedRateLimiter{
rdb: rdb,
maxRequests: maxRequests,
windowSeconds: windowSeconds,
}
}
// Attempt checks and records a request using a Lua script for atomicity.
func (rl *DistributedRateLimiter) Attempt(ctx context.Context, identifier string) (RateLimitResult, error) {
key := fmt.Sprintf("ratelimit:%s", identifier)
now := float64(time.Now().UnixMicro()) / 1e6
windowStart := now - float64(rl.windowSeconds)
script := redis.NewScript(`
local key = KEYS[1]
local window_start = tonumber(ARGV[1])
local now = tonumber(ARGV[2])
local max_requests = tonumber(ARGV[3])
local window = tonumber(ARGV[4])
redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start)
local current = redis.call('ZCARD', key)
if current < max_requests then
redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
redis.call('EXPIRE', key, window)
return {1, max_requests - current - 1}
else
return {0, 0}
end
`)
result, err := script.Run(ctx, rl.rdb, []string{key},
windowStart, now, rl.maxRequests, rl.windowSeconds,
).Int64Slice()
if err != nil {
return RateLimitResult{}, fmt.Errorf("rate limit script: %w", err)
}
return RateLimitResult{
Allowed: result[0] == 1,
Remaining: int(result[1]),
ResetAt: int64(math.Ceil(now)) + int64(rl.windowSeconds),
}, nil
}
using StackExchange.Redis;
namespace Interview;
/// <summary>
/// Result of a rate limit check.
/// </summary>
public readonly record struct RateLimitResult(bool Allowed, int Remaining, long ResetAt);
/// <summary>
/// Distributed rate limiter using Redis sorted sets (sliding window).
/// </summary>
public sealed class DistributedRateLimiter(
IDatabase redis,
int maxRequests,
int windowSeconds)
{
// Lua script for atomic check-and-add
private const string Script = """
local key = KEYS[1]
local window_start = tonumber(ARGV[1])
local now = tonumber(ARGV[2])
local max_requests = tonumber(ARGV[3])
local window = tonumber(ARGV[4])
-- Remove expired entries
redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start)
-- Count current entries
local current = redis.call('ZCARD', key)
if current < max_requests then
-- Add new entry
redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
redis.call('EXPIRE', key, window)
return {1, max_requests - current - 1}
else
return {0, 0}
end
""";
public async Task<RateLimitResult> AttemptAsync(string identifier)
{
var key = $"ratelimit:{identifier}";
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0;
var windowStart = now - windowSeconds;
var raw = await redis.ScriptEvaluateAsync(
Script,
[key],
[windowStart, now, maxRequests, windowSeconds]);
var result = (long[])raw!;
return new RateLimitResult(
Allowed: result[0] == 1,
Remaining: (int)result[1],
ResetAt: (long)Math.Ceiling(now) + windowSeconds);
}
}
import math
import time
from dataclasses import dataclass
from redis.asyncio import Redis
# Lua script for atomic check-and-add
_SCRIPT = """
local key = KEYS[1]
local window_start = tonumber(ARGV[1])
local now = tonumber(ARGV[2])
local max_requests = tonumber(ARGV[3])
local window = tonumber(ARGV[4])
-- Remove expired entries
redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start)
-- Count current entries
local current = redis.call('ZCARD', key)
if current < max_requests then
-- Add new entry
redis.call('ZADD', key, now, now .. ':' .. math.random(1000000))
redis.call('EXPIRE', key, window)
return {1, max_requests - current - 1}
else
return {0, 0}
end
"""
@dataclass(frozen=True)
class RateLimitResult:
allowed: bool
remaining: int
reset_at: int
class DistributedRateLimiter:
"""Sliding window rate limiter backed by Redis sorted sets."""
def __init__(self, redis: Redis, max_requests: int, window_seconds: int) -> None:
self._max_requests = max_requests
self._window_seconds = window_seconds
# register_script caches the SHA and re-uploads on NOSCRIPT
self._script = redis.register_script(_SCRIPT)
async def attempt(self, identifier: str) -> RateLimitResult:
key = f"ratelimit:{identifier}"
now = time.time()
window_start = now - self._window_seconds
allowed, remaining = await self._script(
keys=[key],
args=[window_start, now, self._max_requests, self._window_seconds],
)
return RateLimitResult(
allowed=bool(allowed),
remaining=int(remaining),
reset_at=int(math.ceil(now)) + self._window_seconds,
)
Если вы заучили решение для URL Shortener, но не понимаете, почему выбран Base62, вы не сможете адаптировать решение при изменении требований.
2. Только теория без практики
Чтение книг -- необходимо, но недостаточно. Без практики с таймером и mock interviews знания не конвертируются в навык.
3. Игнорирование soft skills
System Design -- это 50% технические знания и 50% коммуникация. Тренируйте объяснение решений вслух.
4. Перфекционизм
Не нужно знать все до идеала. Достаточно уверенно обсуждать ключевые концепции и честно говорить о границах своих знаний.
Выводы
Долгосрочная подготовка -- это марафон, а не спринт. Постоянная практика по 30 минут в день эффективнее, чем интенсив перед интервью. Инвестируйте в глубокое понимание принципов -- это навык на всю карьеру.