# Backend Architecture Audit 2026-06-25 ## Что хорошо - **Чистая модульная структура** — каждый домен в своём каталоге, NestJS-модули, DI - **Global-модули** (`PrismaModule`, `CacheModule`, `MoexClientModule`) — не дублируются импорты - **`CacheService.getOrFetch()`** — универсальный примитив кеширования, config-driven TTL - **MOEX rate limiter + circuit breaker** — p-queue + ручной CB, защищают от внешнего API - **TBank mappers layer** — proto wire type → domain type → DTO, правильная изоляция - **Batch-оптимизация в PortfolioService** — `fetchShareBatch/fetchBondBatch/fetchDividendsBatch` собирает всё одним MOEX-запросом - **Спецификации и тесты** — 25 spec-файлов, есть TDD-подход ## Ключевые архитектурные проблемы | # | Проблема | Где | Описание | |---|----------|-----|----------| | 1 | **Дублирование Envelope DTO** | Все модули | Каждый модуль переопределяет свой `*ResponseMetaDto / *EnvelopeDto`. 6+ копий одной структуры | | 2 | **Inconsistent caching** | `shares/shares.service.ts:getShare()` (raw) vs `bonds/bonds.service.ts:getBond()` (envelope) | Разное поведение одинаковых по смыслу методов | | 3 | **Screener — in-memory filtering** | `securities/screener.service.ts` | При пустом массиве `getShareMarketDataBatch([])` тянет **все** бумаги с MOEX и фильтрует в памяти. Не масштабируется | | 4 | **MoexClientService — God Service** | 11 публичных методов | Один сервис делает всё: search, shares, bonds, candles, history, dividends. Нарушает SRP | | 5 | **Отсутствие доменных исключений** | Весь код | Нет иерархии исключений (`SecurityNotFoundException`, `PortfolioAccessDeniedException`, `MoexApiException`) — только generic `NotFoundException` / `ForbiddenException` | | 6 | **Health check — заглушка** | `health/` | Не проверяет БД, MOEX, T-Bank. Только `{ status: 'ok', timestamp, uptime }` | | 7 | **Prisma JSON как String** | `targets`, `tags`, `payment`, `price` | JSON хранится как `String` без валидации на уровне БД. Нет типизированных JSON-полей | | 8 | **tbank/ — перегруженный модуль** | 22 файла, 8 сервисов | Один модуль содержит gRPC клиент, мапперы, CRUD, аналитику, синхронизацию. Можно разбить | | 9 | **Auth глобальные гарды** | `JwtAuthGuard` + `RolesGuard` как `APP_GUARD` | Неявная защита всех эндпоинтов. Приходится использовать `@Public()` для открытых | | 10 | **RequestLoggingMiddleware** | Подключён через `.use()`, а не через `configure()` | Работает, но не идёт через DI и не является частью модуля | ## Рекомендации ### 🔴 Critical 1. **Shared envelope DTO** — вынести `ApiResponseMeta` и один generic `EnvelopeDto` в `common/dto/`, убрать дублирование. Унифицировать формат ответа screener'а под общий envelope. 2. **Разделить MoexClientService** — выделить `MoexSecuritiesClient`, `MoexMarketDataClient`, `MoexCandlesClient` — каждый со своим набором методов. 3. **Убрать inconsistency shares vs bonds** — `getShare()` должен возвращать `ApiEnvelopePayload` как и `getBond()`. ### 🟡 Medium 4. **Domain exception hierarchy** — создать `BaseDomainException` → `MoexApiException`, `SecurityNotFoundException`, `PortfolioAccessDeniedException`, `TBankApiException`. Добавить соответствующие фильтры в `HttpExceptionFilter`. 5. **Screener — server-side пагинация** — кешировать полный результат screener'а отдельным TTL. 6. **Health check прокачка** — добавить проверки Prisma (`db.ping()`), MOEX (`/health`), T-Bank gRPC connectivity. 7. **Prisma JSON → typed JSON** — использовать строки с `JSON.parse` в геттерах или перейти на отдельные таблицы. 8. **Отвязать `securities/` от прямого импорта `CacheModule`** — раз он `@Global()`, убрать лишний импорт. ### 🟢 Low / Nice to have 9. **RequestLoggingMiddleware** — перевести на `configure()` в `AppModule` для единообразия. 10. **Refactor `tbank/`** — выделить `broker-analytics` и `broker-sync` в отдельные модули, если будут расти. 11. **Swagger schema object для обёртки** — глобально настроить OpenAPI для автоматической обёртки `{ data, meta }`. 12. **Circuit breaker — вынести в декоратор** — обобщить в `@CircuitBreaker()` декоратор.