Архитектурные принципы

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) проверяется на трёх уровнях:

  1. 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.

  2. 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 + alert privacy_invariant_violated_total (никогда не тихая ошибка).

  3. 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.