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 обычно включает следующие этапы:
-
Анализ входов: изучение HLD - границ компонента, его ответственности, контрактов с соседями и распределённых NFR. Если HLD неполон, LLD начинается с уточнения пробелов.
-
Проектирование модуля: разбиение компонента HLD на внутренние модули/слои (domain, application, infrastructure) и определение их публичных интерфейсов. Здесь применяются принципы SOLID и паттерны проектирования.
-
Проектирование классов и объектов: определение классов/интерфейсов, их атрибутов, методов и отношений. Результат - диаграмма классов (UML) уровня модуля.
-
Проектирование данных: детальная схема БД - таблицы, поля с типами, индексы, внешние ключи, миграции. На уровне HLD была только концептуальная модель; здесь она превращается в физическую.
-
Определение контрактов API: спецификация эндпоинтов/методов, запросов и ответов, кодов ошибок, версионирования (OpenAPI/gRPC).
-
Определение алгоритмов и обработка ошибок: проработка ключевых алгоритмов и сценариев - включая обработку ошибок, граничные случаи (пустые выборки, дубли, таймауты, конкурентный доступ). Для критичных сценариев строятся диаграммы последовательностей.
-
План тестирования: определение модульных и интеграционных тестов, тестовых случаев - положительных и отрицательных. LLD должен быть таким, чтобы по нему можно было написать тесты до или параллельно с кодом (см. TDD).
-
Документирование: оформление 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.