Архитектурные принципы
NOTE
Статус: Target design. Документ описывает целевую архитектуру. Сервисы, модули и контракты, упомянутые ниже, могут ещё не существовать в
backend/. Правила маркировки — в50-processes/documentation-standard.md.
Обязательные к соблюдению принципы. Отступление — только через ADR.
P1. Разделение слоёв данных
Три уровня: Raw Payload → Supplier Offer → Canonical Product. Нельзя смешивать в одной сущности. См. ../10-business/domain-model.md.
P2. Источник истины — PostgreSQL
Все первичные данные (canonical products, offers, event store, справочники) живут в PostgreSQL. ES, ClickHouse, кэши — проекции. Любую проекцию можно пересоздать из PG.
P3. Чистая архитектура модулей
Domain не зависит ни от чего, app — только от domain, infra реализует порты app, api — тонкий слой. Детали: module-layout.md.
P4. Event Sourcing для canonical
Все изменения canonical products, manufacturer, identity profiles — через события. Snapshot для быстрого восстановления. Цены/остатки — вне ES.
P5. Outbox pattern для публикации событий
Запись события в event_store и outbox — в одной транзакции. Публикация в Kafka — отдельным процессом. Гарантируем at-least-once доставку.
P6. Category-agnostic
В доменной модели нет иерархии категорий. Классификация — через classification_tag (неструктурированные теги) и Identity Profile (структурированные предикаты критических характеристик).
P7. Rate Budget централизован
Весь rate limiting — в Redis, через token bucket, конфигурируемо per-supplier. Коннектор не решает, сколько запросов делать; он запрашивает токены у центрального limiter.
P8. Прозрачность поиска
Каждый результат поиска в админке — с _explain. Маппинги, аналайзеры, синонимы — в git. Изменения через PR и интеграционные тесты с golden set.
P9. Ничего не пишем молча
AI-enrichment, эвристический matching, автоматические решения — всегда с confidence, source, возможностью отката. UI показывает источник данных.
P10. Документация — часть DoD
PR не мёрджится, если:
- новая фича не отражена в документации;
- новая таблица / индекс / топик не в
schemas/; - нетривиальное решение без ADR.
P11. Идемпотентность ingestion
Любая операция ingestion идемпотентна по (supplier_id, supplier_sku, version). Повторное получение того же payload’а не создаёт дублей.
P12. Graceful degradation
Падение одного поставщика / одного хранилища-проекции не должно ронять систему. Обязательные circuit breakers и fallback’и (для ES — fallback на PG search, для ClickHouse — skip analytics).
P13. Explicit versioning
Schemas событий имеют версию в имени топика (.v1, .v2). OpenAPI имеет версию в URL. Миграция — через двойную публикацию и плавный переход.
P14. AI — инструмент, не хозяин
AI-выводы всегда кэшируются (learned cache), всегда с confidence, всегда проверяются на выходе. AI не имеет силент-записи в canonical.
P15. Everything observable
С первого дня: OpenTelemetry tracing, structured logging, per-supplier метрики. Без этого — нельзя ловить деградации в ingestion.
P16. Multi-tenant supplier credentials
API поставщика — context-sensitive endpoint. Доступ моделируется явно через SupplierCredential (scope system | customer). Цены / остатки — это Observation, привязанная к credential, а не свойство SupplierOffer.
Pricing engine выбирает observation: own customer credential → системная → fallback. Observations клиента A никогда не используются для pricing клиента B (privacy invariant).
Rate budget — per-credential. Дедупликация одинаковых credentials через SupplierCredentialGroup экономит rate budget. Credentials поддерживают разные auth-схемы (login/pass, API key, OAuth, bearer), хранятся в БД с app-level encryption секретов. См. ADR-0009, ADR-0010, ADR-0018.
Privacy invariant — enforcement
Privacy invariant (observation_X.customer_ref ≠ pricing_request.customer_id ⇒ observation НЕ используется, кроме случая group membership) проверяется на трёх уровнях:
-
Repository layer (compile-time guarantee): метод
ObservationRepository.QueryForPricing(ctx, request_customer_id)НЕ принимаетcustomer_idпараметр иначе как изrequest_customer_idконтекста. ПрямойSELECT FROM supplier_offer_observationзапрещён вне репозитория. Архитектурный CI-линтер (go-arch-lintили собственный AST-линтер) проверяет, что только пакетcore/pricing/repoимпортирует низкоуровневую модель observation. -
Runtime guard: каждое observation, попадающее в pricing pipeline, помечается tag’ом
customer_scopeв context-tracker. Перед записью в response — assert, что для каждой строкиobservation.customer_scope ∈ {request.customer_id, ∅(system), group_members(request.customer_id)}. Нарушение — panic + alertprivacy_invariant_violated_total(никогда не тихая ошибка). -
Property-based test:
pkg/pricing/test/property_privacy_test.go— генерирует случайные multi-customer observations, прогоняет pricing для customer_X, проверяет, что в результате нет данных от customer_Y (Y ≠ X, Y не в группе X). Запускается в CI на каждый PR в pricing/credentials/visibility модулях.
Метрика privacy_invariant_violated_total — алерт немедленный, severity P0.
P17. No-proxy architecture
Мы — самостоятельная система с собственным обогащённым каталогом, не транзитный прокси к поставщикам. Клиентские запросы никогда не порождают синхронные запросы к поставщику. Все запросы к поставщикам — в фоновых задачах (scheduled / on-demand async). Клиент получает данные только из нашего хранилища.
Следствия: предсказуемая latency, контролируемый response shaping через Visibility Policy, защита API поставщика от пиков, кэшируемость. См. ADR-0011.
P18. Data Visibility Policy для всех точек выдачи
Видимость данных управляется первоклассной подсистемой DataVisibilityPolicy, а не ad-hoc правилами в коде сервисов. Применяется единообразно во всех точках выдачи: search, pricing, estimate, API response shaping, analytics, webhooks.
Policies версионируются, audit-trail event-sourced, есть симулятор “что увидит клиент X”. См. ADR-0012.
P19. Suppliers as graph
Поставщик — узел графа с типизированными roles[] (api_provider, warehouse_operator, sales_agent, manufacturer, distributor, aggregator, logistics) и связями SupplierRelationship (aggregates, resells, is_agent_of, uses_api_of, uses_warehouse_of, subsidiary_of, exclusive_for).
Каждый offer хранит SupplyChainTrace — вычисленную цепочку до реального производителя. Используется для: discovery альтернативных источников, pricing breakdown с маржинальной структурой, trust-based conflict resolution, visibility policies по реальному manufacturer, audit/compliance.
См. ADR-0013.
P20. Graceful degradation без прогрева
Клиентский запрос никогда не блокируется ожиданием поставщика. Ответ всегда возвращается на основе имеющихся данных:
- Нет данных → “не найдено” + async DiscoveryJob.
- Старые данные → возвращаем с пометкой
stale=true+ RefreshJob. - Частичные данные → возвращаем что есть с пометкой “incomplete”.
SLA на ответ и SLA на свежесть — независимые контуры. Деградация поставщика влияет только на свежесть, не на доступность сервиса. См. ADR-0014.
P21. Multi-protocol public API
Внешний API экспонируется тремя протоколами: REST (HTTP+JSON), gRPC, WebSocket. Все они — поверх общего ядра use cases (без дублирования бизнес-логики). Унифицированный паттерн: read cache + trigger refresh + watch updates на каждом протоколе.
См. ADR-0015.
P22. Расширяемые словари с known semantics
Множества типов, расширяемые без deploy (например, supplier_role.kind, supplier_relationship.kind), реализуются как БД-словари с обязательным semantic_handler — машинной семантикой, известной коду. Несколько code могут разделять один semantic; неизвестный semantic → fallback handler GENERIC.
Trust level и подобные критичные для conflict resolution поля остаются фиксированными enum’ами (расширение через ADR).
См. ADR-0016.
P23. Event-driven cross-aggregate recalc
Любые “каскадные” пересчёты между агрегатами (например, SupplyChainTrace при изменении SupplierRelationship) — через события в Kafka, не через прямые вызовы. Дополнительно — scheduled rebuilder для защиты от пропущенных событий.
См. ADR-0017.
P24. Stdlib-first transport stack
Новые Go-сервисы используют net/http и http.ServeMux как базовый HTTP router. RPC и streaming поверх HTTP реализуются через Connect, WebSocket — через github.com/coder/websocket.
gorilla/mux, gorilla/websocket и альтернативные router/mux-стэки не используются без отдельного ADR. См. ADR-0021.
P25. Единый DI runtime через uber-go/fx
Композиционный корень каждого Go-сервиса — fx.App, собранный из модулей. cmd/<service>/main.go содержит только fx.New(...) + app.Run(); никакая другая логика (построение сервисов, readiness, FS-поиск, signal-обработка) в main.go не живёт.
Платформенные модули (config, logger, pgxpool, kafka, redis, openapi, health, http) — в backend/internal/platform/di. Каждый bounded context предоставляет свой Module() в backend/internal/core/<bc>/di.go. Ресурсы с внешним соединением регистрируют fx.Lifecycle hooks; graceful shutdown управляется fx.
Самодельные сервис-локаторы, map/constant-based контейнеры, скрытые глобальные singleton-зависимости и package-level init(), делающий IO-работу, запрещены. Тесты на композицию подменяют зависимости через fxtest + fx.Replace с mockery-моками.
CLI-утилиты без внешних ресурсов (cmd/errorlint и т.п.) — исключение: fx там избыточен.
См. ADR-0030 (действующее решение), ADR-0021 (stdlib-first transport) и ADR-0023 (mockery).
P26. Compose-network first runtime communication
Все deployable-сервисы внутри среды общаются по общей сети docker-compose через DNS-имена сервисов (catalog-core, pricing, postgres, kafka и т.д.). Host ports публикуются только для ingress, браузера разработчика и операционной диагностики.
Внутренние синхронные вызовы — по internal HTTP/Connect endpoint’ам, асинхронные — через Kafka. См. ADR-0021 и ../40-operations/deployment.md.
P27. Docs-as-code и web-доступность обязательны
Каноническая документация живёт в репозитории, собирается через Quartz и публикуется в web через GitLab Pages. Любое изменение поведения, контракта, схемы, deploy-контура, CI/CD или процесса должно обновлять документацию в том же MR.
Документация не версионируется параллельными деревьями; web-сайт всегда показывает актуальное состояние main. См. ADR-0022.
P28. Нормальная пирамида тестирования
Основной объём покрытия — unit-тесты, затем service/integration, сверху — ограниченный набор smoke/E2E. Для внутренних портов используются только сгенерированные mockery-моки; hand-written mocks для нового кода запрещены.
Внешние провайдеры покрываются контрактными тестами на моковых HTTP-ответах, записанных в fixtures из реальных или sandbox API-ответов. См. ADR-0023.
P29. Product versioning и changelog с первого дня
Продукт версионируется по SemVer. Источник истины версии — файл VERSION и git tag формата vX.Y.Z. История изменений ведётся в CHANGELOG.md по Keep a Changelog, release notes генерируются в GitLab CI через git-cliff, а типы коммитов следуют Conventional Commits.
Версионируется продукт и контракты, но не параллельные копии документации. См. ADR-0022.
P30. Sub-module boundaries match aggregates; siblings isolated
В каждом bounded context ровно одна раскладка: тонкое ядро + суб-модули, где 1 агрегат = 1 суб-модуль (или use-case-bounded группа с явным обоснованием). Соседние суб-модули не импортируют пакеты друг друга — взаимодействие через consumer-owned порты + проводку в core/<bc>/di.go. Правило машинно проверяется go-arch-lint (.go-arch-lint.yml генерируется из дерева). Полный контекст — ADR-0031.