Low-Level Design

Общее

Low-Level Design (LLD) - это этап проектирования, на котором общая архитектура, определённая на High-Level Design (HLD), детализируется до реализуемых модулей. LLD отвечает на вопрос как именно устроен каждый компонент внутри: классы и их методы, алгоритмы, структуры данных, схема базы данных, контракты API, обработка ошибок и граничные случаи.

Главный признак LLD - уровень детализации. Если HLD описывает систему как совокупность «чёрных ящиков» (что делает компонент и как связан с другими), то LLD «раскрывает» каждый ящик: показывает внутреннее устройство так, чтобы разработчик мог писать код без дополнительных вопросов к архитектору. LLD принимает архитектуру HLD как данность и не переопределяет её.

LLD выполняется после HLD и опирается на:

  • границы компонентов и их ответственность (из HLD);
  • контракты взаимодействия между компонентами (из HLD);
  • нефункциональные требования, распределённые по компонентам (из HLD).

Результат LLD - набор спецификаций, по которым ведётся кодирование. Хороший LLD уменьшает неоднозначность: разные разработчики, читая одну и ту же спецификацию, напишут семантически близкий код.

Что входит в LLD

Состав артефактов LLD зависит от компонента, но ядро устойчиво:

Артефакт Что описывает Типовой инструмент
Спецификация модуля назначение модуля, его публичный интерфейс, зависимости, инварианты текстовая спецификация, JSDoc/Javadoc
Диаграмма классов классы/интерфейсы модуля, их атрибуты, методы и отношения (наследование, ассоциации) UML class diagram
Диаграмма последовательностей обработка конкретных сценариев: кто кого вызывает и в каком порядке UML sequence diagram
Схема БД (ERD) таблицы, поля с типами, индексы, внешние ключи, ограничения ER-диаграмма, DDL-миграции
Контракты API эндпоинты/методы, запросы/ответы, коды ошибок, версии OpenAPI (REST), .proto (gRPC)
Алгоритмы и структуры данных ключевые алгоритмы (поиск, агрегация, конкурентный доступ) и структуры данных (списки, деревья, хеш-таблицы) псевдокод, блок-схемы
Обработка ошибок и edge cases что считается ошибкой, как она обрабатывается и возвращается пользователю/вызову таблицы кодов ошибок, exception-иерархии
Тест-план что и как проверяется: модульные, интеграционные тесты, тестовые случаи тест-кейсы, xUnit-спецификации

Важно: выбор архитектурного стиля, разбиение на компоненты и контракты между ними - это артефакты HLD, не LLD. LLD их принимает как вход и детализирует внутреннее устройство каждого компонента.

Процесс

Процесс работы над LLD обычно включает следующие этапы:

  1. Анализ входов: изучение HLD - границ компонента, его ответственности, контрактов с соседями и распределённых NFR. Если HLD неполон, LLD начинается с уточнения пробелов.

  2. Проектирование модуля: разбиение компонента HLD на внутренние модули/слои (domain, application, infrastructure) и определение их публичных интерфейсов. Здесь применяются принципы SOLID и паттерны проектирования.

  3. Проектирование классов и объектов: определение классов/интерфейсов, их атрибутов, методов и отношений. Результат - диаграмма классов (UML) уровня модуля.

  4. Проектирование данных: детальная схема БД - таблицы, поля с типами, индексы, внешние ключи, миграции. На уровне HLD была только концептуальная модель; здесь она превращается в физическую.

  5. Определение контрактов API: спецификация эндпоинтов/методов, запросов и ответов, кодов ошибок, версионирования (OpenAPI/gRPC).

  6. Определение алгоритмов и обработка ошибок: проработка ключевых алгоритмов и сценариев - включая обработку ошибок, граничные случаи (пустые выборки, дубли, таймауты, конкурентный доступ). Для критичных сценариев строятся диаграммы последовательностей.

  7. План тестирования: определение модульных и интеграционных тестов, тестовых случаев - положительных и отрицательных. LLD должен быть таким, чтобы по нему можно было написать тесты до или параллельно с кодом (см. TDD).

  8. Документирование: оформление LLD в виде спецификаций (docs-as-code рядом с кодом) и ревью командой.

Дополнительные факторы

В процессе работы над LLD необходимо учитывать:

  • Внимательность к деталям: LLD работает на уровне, где опечатка в типе или пропущенный индекс становятся багом в проде. Каждый сценарий стоит продумывать «до конца», включая ошибочные пути.

  • Соответствие HLD и NFR: LLD не должен нарушать границы и контракты, заданные HLD, и обязан учитывать распределённые NFR. Например, если HLD зафиксировал p99 ≤ 300 мс, LLD обязан показать индексы и кеш, которые это обеспечивают.

  • Использование проверенных подходов: применение паттернов проектирования, SOLID и стандартов вместо «изобретения велосипеда». Это снижает риск и повышает читаемость.

  • Тестируемость с самого начала: LLD проектируется так, чтобы компонент было легко тестировать - зависимости инъектируются, побочные эффекты изолируются, чистая логика отделяется от I/O.

  • Документирование и код: спецификации LLD живут рядом с кодом и обновляются вместе с ним. «Комментарии в коде» - не замена спецификации: они описывают «как», а LLD объясняет «почему так и какие сценарии».

  • Итеративность: LLD редко пишется «один раз и навсегда». По мере кодирования и тестирования вскрываются детали, которые возвращают к уточнению спецификации. Это нормально; важно держать документ и код синхронными.

Плюсы

Снижает неоднозначность кодирования: разработчик получает ясные спецификации и пишет код, не возвращаясь с вопросами к архитектору. Это ускоряет работу и уменьшает переделки.

Выявляет проблемы до кода: ошибки в алгоритмах, схеме БД, контрактах API видны на уровне LLD, где их исправить дешевле, чем в написанном коде.

Улучшает качество и тестируемость: LLD, спроектированный с учётом тестирования, даёт чистый, слабосвязанный код, который проще покрывать тестами и поддерживать.

Обеспечивает согласованность: единые спецификации снижают разброс реализаций - разные разработчики пишут семантически близкий код.

Ускоряет онбординг: по LLD новый разработчик быстрее входит в компонент, чем разбираясь в «голом» коде.

Минусы

Трудоёмкость: детальное проектирование требует времени и сил, что может увеличить срок начала кодирования и стоимость разработки.

Необходимость поддержки: LLD устаревает, если не синхронизировать его с кодом. Рассинхрон документации и реализации хуже, чем отсутствие документации.

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

Невозможность предсказать все сценарии: часть граничных случаев и нюансов проявляется только при кодировании и тестировании. LLD не может и не должен быть исчерпывающим.

Ограничение гибкости: слишком жёсткий LLD может мешать разработчику выбирать лучшие локальные решения. Баланс между спецификацией и свободой - ответственность автора LLD.

Когда применять

Для сложных и критичных компонентов: модули с богатой бизнес-логикой, финансовые расчёты, обработка персональных данных, конкурентный доступ - здесь цена ошибки высока и LLD окупается.

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

В проектах с высокими NFR: highload, требования к latency, надёжности, безопасности - эти свойства «прорабатываются» именно на уровне LLD (индексы, кеши, блокировки, ретраи).

При добавлении новой функциональности в существующую систему: LLD новой фичи гарантирует её встраивание в текущую архитектуру без поломок.

Когда достаточно кода: для простой утилиты или тривиального CRUD-модуля формальный LLD может быть избыточен - достаточно тестов и короткого описания в README.

Кто пишет и ревьюит

Роль Ответственность
Tech Lead / ведущий разработчик компонента Автор LLD: отвечает за внутреннее устройство модуля
Команда разработки Ревью: реализуемость, согласованность с neighbouring кодом
Архитектор / Solution Architect Ревью: соответствие HLD, границы и контракты не нарушены
QA / тестировщик Ревью: тест-план, покрытие сценариев и ошибочных путей
DBA (при существенной схеме) Ревью: индексы, типы, миграции, производительность запросов

Критерии готовности (DoD)

LLD считается готовым к передаче в кодирование, когда:

  • описаны все модули/классы и их интерфейсы, нет «белых пятен»;
  • определены схемы БД: таблицы, типы, индексы, внешние ключи, миграции;
  • зафиксированы контракты API (эндпоинты, запросы/ответы, ошибки);
  • проработаны ключевые алгоритмы и сценарии, включая обработку ошибок и граничные случаи;
  • есть тест-план: модульные и интеграционные тесты, положительные и отрицательные случаи;
  • LLD соответствует HLD: границы и контракты не нарушены, NFR учтены;
  • спецификации прошли ревью команды и архитектора;
  • документация хранится рядом с кодом и будет поддерживаться актуальной.

Пример

Продолжим пример из HLD - онлайн-магазин. Компонент Backend API на уровне HLD - «чёрный ящик». LLD «раскрывает» его на примере модуля заказов:

Классы (диаграмма классов):

  • OrderService - доменный сервис; методы: placeOrder(cartId, customerId), cancel(orderId), getById(orderId). Зависит от OrderRepository, PaymentGatewayClient, EventPublisher.
  • Order (aggregate root) - инкапсулирует позиции, статус, инварианты (нельзя оплатить отменённый заказ).
  • OrderItem (value object) - позиция заказа: товар, количество, цена.
  • OrderRepository (interface) - save(order), findById(id); реализация SqlOrderRepository.
  • PaymentGatewayClient (interface) - charge(amount, token); реализация HttpPaymentGatewayClient.
  • EventPublisher (interface) - publish(event); реализация BrokerEventPublisher.

Схема БД (ERD):

  • orders (id, customer_id, status, total_amount, created_at) - индекс на (customer_id, created_at).
  • order_items (id, order_id, product_id, qty, unit_price) - внешний ключ order_id, индекс на order_id.
  • payments (id, order_id, gateway_txn_id, amount, status, paid_at) - внешний ключ order_id, уникальный gateway_txn_id.

Контракт API (REST, OpenAPI):

  • POST /orders - тело {cartId, customerId, paymentToken}, ответ 201 {orderId, status}, ошибки 400 (невалидные данные), 402 (платёж отклонён), 409 (конфликт, например, корзина уже оформлена).
  • GET /orders/{id} - ответ 200 {id, status, items, total}, ошибки 404 (не найдено), 403 (нет прав).

Обработка ошибок и edge cases:

  • повторный POST /orders с тем же cartId - идемпотентность через уникальный токен, возврат существующего заказа;
  • таймаут платёжного шлюза - ретрай с экспоненциальной задержкой, затем статус payment_pending и фоновый компенсирующий процесс;
  • попытка оплатить отменённый заказ - 409;
  • гонка двух запросов на один заказ - оптимистичная блокировка по version.

План тестирования:

  • модульные тесты на Order и OrderService (моки репозитория и шлюза);
  • интеграционные тесты на SqlOrderRepository с тестовой БД;
  • тесты контракта API (положительные и все коды ошибок);
  • тесты конкурентного доступа (два параллельных placeOrder на одну корзину).

Заметьте: здесь нет ответа на вопрос «из каких крупных компонентов состоит система» - это HLD. LLD описывает, как устроен один конкретный компонент.

Сравнение HLD и LLD

Краткая версия - подробное сравнение см. в статье HLD:

Ось HLD LLD
Главный вопрос Из каких частей состоит система и как они связаны Как устроена каждая часть внутри
Уровень «Чёрные ящики» - что делает компонент «Белый ящик» - классы, методы, таблицы, алгоритмы
Артефакты Component/Deployment/Context, концептуальная модель, контракты между компонентами Диаграммы классов/последовательностей, ER-схема БД, контракты API, обработка ошибок, тест-план
Кто пишет Архитектор Tech Lead / ведущий разработчик компонента
Когда выполняется После требований, до LLD После HLD, до/параллельно с кодированием

См. также

  • High-Level Design - предыдущий уровень проектирования, чьи границы и контракты LLD детализирует.
  • System Design - более широкий процесс проектирования систем.
  • SOLID - принципы объектно-ориентированного проектирования, применяемые на уровне LLD.
  • Паттерны проектирования - типовые решения, используемые внутри модулей.
  • Architecture Decision Records - фиксация решений, на которые опирается LLD.