refactor: organize backend by business responsibility

This commit is contained in:
2026-09-13 13:52:57 +03:00
parent 7fde3ece6e
commit 8ba005346d
145 changed files with 3027 additions and 2346 deletions
+194 -26
View File
@@ -1,39 +1,207 @@
# Backend architecture
## Выбор владельца
## Module-first organization
Найди существующий use case, его collaborators и tests до создания module/service.
Сохрани ответственность в текущем module, если новая возможность относится к нему.
Module — граница связного набора providers, а не папка для каждого endpoint.
Backend — domain-oriented modular monolith с лёгким разделением
по ответственности. `src/modules` группирует код по бизнес-возможности: `auth`, `banking/bank-cards`, области `investments`.
В инвестициях отдельные Nest modules владеют `brokerage-accounts`, `portfolio`,
`operations`, `income`, `upcoming-events`, `instruments`, `tbank-connection`.
`instruments/stocks` и `instruments/bonds` — slices внутри одного модуля.
| Решение | Owner |
### Progressive structure
Три уровня организации: business module → responsibility layer → technical grouping
при реальной необходимости. Компактный module с понятными controller/DTO/service
может оставаться плоским. Ориентир 5–7 файлов не является порогом: учитывай число
ответственностей, наличие domain/repository/use cases и удобство просмотра каталога.
Когда в module смешаны несколько services, transport, расчёты и persistence,
группируй файлы по ответственности:
```text
feature/
├── feature.module.ts
├── feature.api.ts # только при существующих внешних потребителях
├── presentation/ # HTTP, DTO, guards
├── application/ # сценарии, orchestration, application input
├── domain/ # чистые бизнес-правила и расчёты
└── infrastructure/ # persistence, framework и boundary mapping
```
Создавай только нужные слои. В корне сложного module оставляй composition и
публичную boundary; production code и colocated tests размещай в слое владельца.
Внутри business slice действует то же правило, без отдельного Nest module ради папок.
`controllers/`, `dto/`, `services/`, `use-cases/`, `persistence/`, `mappers/`
допустимы только внутри слоя, когда множество однотипных файлов мешает навигации.
Для одного controller или пары DTO дополнительная поддиректория не нужна.
Пустые папки, новые barrels, CQRS и interfaces не требуются для этой организации.
В текущем коде слои используют `auth`, `bank-cards`, `portfolio`, `operations`
и slices `instruments/stocks`, `instruments/bonds`. Общие application helpers
инструментов находятся в `instruments/application`; `instruments.module.ts`
компонует оба slice. `stock-position.ts` и `instrument-position.ts` формируют
результат из portfolio DTO; `instrument-market.ts` обрабатывает внешние данные
и ошибки Nest. Это application code, а не независимая domain model.
`brokerage-accounts`, `income`, `upcoming-events`, `tbank-connection` остаются
компактными плоскими modules. Наличие tests само по себе не требует слоёв.
## Поиск и создание файла
При работе над feature проходи владельцев в порядке:
1. Найди business module.
2. Прочитай его `*.module.ts` (для slice — composition родительского module).
3. Проверь `presentation/`: controller и transport contract.
4. Проследи вызовы в `application/`: сценарии и services.
5. Проверь используемые правила в `domain/`.
6. Проверь `infrastructure/`: repository и технические реализации.
7. По обнаруженным зависимостям переходи в `integrations/` и `common/`.
В плоском module классифицируй файлы по suffix и следуй их ответственности.
Отсутствующий слой пропускай. Если business owner известен, начинай локально,
а не с repository-wide search.
**Rule of locality:** code that belongs to one business capability stays inside
that capability unless it is cross-cutting. Например, расчёты карт принадлежат
bank-cards/domain, даже если реализованы обычными функциями.
Перед созданием файла определи:
1. Какой business module владеет изменением? Сначала owner, затем common при
подтверждённой общей потребности.
2. Какова ответственность: HTTP/DTO/guard → presentation; сценарий/orchestration →
application; чистое business rule/calculation → domain; persistence или
module-specific technical implementation → infrastructure; внешний протокол → integrations.
3. Какой approved suffix из таблицы соответствует этой ответственности?
4. Какой слой владеет файлом? В компактном плоском module сохрани размещение рядом
с owner; в слоистом используй соответствующий слой.
5. Достаточно ли colocate файл рядом с существующими? Level 3 grouping создавай
только при заметном улучшении навигации по множеству однотипных файлов.
Если owner или роль не определены, сначала установи ответственность. Generic
`utils.ts`, `helpers.ts`, `manager.ts`, `handler.ts` и arbitrary directory её не заменяют.
## Naming & File Suffix Convention
Имена файлов — `kebab-case`. File suffix определяет responsibility, directory
определяет architectural layer. Размещение в таблице предпочтительно для
слоистого module; компактный плоский module сохраняет файлы в корне.
Cross-module consumers во всех строках ограничены публичной границей ниже.
| Suffix | Когда использовать | Где | Кто зависит | Когда не использовать |
| --- | --- | --- | --- | --- |
| `*.controller.ts` | HTTP endpoint / transport | presentation | Nest wiring | DB, расчёты, SDK, сложная оркестрация |
| `*.dto.ts` | Request/response, validation, OpenAPI | presentation | Controller; application boundary при совпадении shape | Универсальная domain model |
| `*.use-case.ts` | Один самостоятельный application scenario | application | Controller, другой orchestrator | Prisma queries, Request/Response, SDK implementation |
| `*.service.ts` | Конкретная cohesive reusable capability | рядом с owner; application/domain/integration по роли | Сценарии и services; domain — только от чистого domain service | Свалка несвязанных сценариев |
| `*.repository.ts` | Persistence queries и согласованные DB reads/writes | infrastructure | Application service/use case | Domain calculations, HTTP request, external transport |
| `*.client.ts` | Низкоуровневый внешний HTTP/gRPC/SDK | integrations или infrastructure | Application orchestration, boundary mapper | Пользовательский lifecycle и domain rules |
| `*.mapper.ts` | Meaningful conversion между моделями | обычно infrastructure у преобразуемой boundary | Код соответствующей boundary | Формальное копирование нескольких одинаковых полей |
| `*.policy.ts` | Cohesive decision rule | domain/application/integration по входным данным | Владелец правила; domain — от чистой policy | Persistence, transport, generic strategy |
| `*.guard.ts` | Nest guard | presentation | Nest/controller | Основная account authorization и финансовые правила |
| `*.strategy.ts` | Framework strategy, например JWT | infrastructure | Framework wiring | Обычный service без strategy contract |
| `*.module.ts` | Nest imports/providers/controllers/exports | module root | Composition root, importing module | Бизнес-логика |
| `*.types.ts` | Малая группа связанных TS types | рядом с owner | Потребители модели на разрешённых слоях | Глобальные types всего module, runtime validation |
| `*.api.ts` | Явный список публичных exports | root конкретного module/integration | Другие modules | DTO/client/service implementation или generic API logic |
| `*-input.ts` | Application input: нормализация и проверка формы, отличающейся от wire DTO | application | Application code | Копия DTO под новым именем; вариант `*.input.ts` |
| `*.calculations.ts`, `*.rules.ts` или предметное имя | Чистые бизнес-расчёты, value semantics | domain | Application и domain | Nest/Prisma/HTTP/SDK зависимости |
| `*.spec.ts` | Существующая unit/component проверка поведения | рядом с owner/slice | Test runner | Архитектурные проверки папок и trivial wiring |
`bank-card-input.ts` использует выбранный `*-input.ts`: переводит Money в
копейки, нормализует значения и проверяет application input. DTO не дублируется
отдельным Input type. `rub-money.ts` — domain-правила RUB; `bond-money.mapper.ts`
— преобразование ответа клиента. Эти роли не определяются словом money в имени.
Технические framework suffixes (`*.filter.ts`, `*.middleware.ts`,
`*.decorator.ts`, `*.config.ts`, `*.validation.ts`) сохраняют соответствующие
существующие роли в common/config. `tbank.crypto.ts` — локальная реализация
шифрования токена; имя не является разрешением создавать generic crypto layer.
HTTP/e2e naming и расположение — в [Testing](backend-testing.md).
Без зафиксированной необходимости не вводи `*.helper.ts`, `*.utils.ts`,
`*.manager.ts`, `*.processor.ts`, `*.handler.ts`, `*.provider.ts`, `*.factory.ts`.
Новая naming convention сначала получает конкретную роль и обоснование здесь.
Для data access выбран только `*.repository.ts`, не persistence/store/dao.
## Публичные границы
`*.module.ts` задаёт DI boundary; `*.api.ts` перечисляет используемые другими
модулями providers, функции и типы. Внешний production import идёт через этот
API или файл Nest module. Чистый `integrations/tbank/decimal.ts` — отдельный
публичный вход для protobuf conversion: импорт арифметики не загружает DI/Prisma.
Внутри модуля используй прямые локальные импорты.
Tests могут импортировать внутренний owner для проверки поведения и mocks.
Новый export требует текущего потребителя; controller и persistence остаются
приватными. Не добавляй `index.ts` на каждом уровне.
Каждый provider регистрируется у одного владельца. Потребители импортируют
его module, а не повторяют provider: это сохраняет единственный кэш и лимитер.
`AuthModule` экспортирует guard; наличие JWT не заменяет account access check.
- `income` читает операции через `operations.api.ts`.
- `instruments` получает личные позиции через `portfolio.api.ts`.
- `tbank-connection` использует T-Bank integration и публичный сброс портфеля.
- `portfolio` и другие инвестиционные модули используют `TbankModule`, который
не зависит от business modules. Так управление подключением не образует цикл.
- `investment-calendar.ts` и `account-reference.dto.ts` разделяются только
инвестиционными областями; это не глобальная библиотека бизнес-правил.
## Ответственность и направление зависимостей
| Владелец | Ответственность |
| --- | --- |
| Route, request/response mapping, status/cookie | Controller, [HTTP rules](backend-http-api.md) |
| Business use case, orchestration, domain validation | Feature service |
| Input shape/format | DTO + configured ValidationPipe |
| Атомарность/DB query | Сервис use case через PrismaService, [Data](backend-data.md) |
| Внешний protocol, deadline/retry/limiting | Existing client, [Integrations](backend-integrations.md) |
| Чистое вычисление/mapper без lifecycle и DI | Обычная функция рядом с domain owner |
| Bootstrap/config/global cross-cutting concern | AppModule, config, существующий common owner |
| Controller / presentation | Route, verified identity, transport DTO, validation, status/cookie; вызов сценария |
| Use case / application service | Оркестрация, business validation, доступ и согласованность результата |
| Domain | Чистые правила и расчёты; без NestJS, Prisma, HTTP/Swagger и внешних SDK |
| Persistence / infrastructure | Запросы, транзакции, технические реализации границ |
| `src/integrations/{tbank,moex,ncr}` | Внешние протоколы, декодирование, retry/deadline/cache; без зависимостей от HTTP DTO business modules |
| `src/prisma` | Общий DB provider и lifecycle; не generic repository |
**Безопасный путь:** расширь текущий owner; выделяй collaborator, когда он скрывает
самостоятельный инвариант или координацию. Не вводи CQRS, generic repository,
interface с одной реализацией или base controller только ради шаблона Nest.
Не превращай common в хранилище бизнес-логики и не переноси frontend модели сюда.
Основной вызов: presentation → application → domain; application использует
конкретные infrastructure providers, зарегистрированные module. Infrastructure
может использовать application types/input и domain rules; новые interfaces
ради инверсии зависимостей не обязательны. Domain не импортирует presentation или
infrastructure. Существующие связные CRUD/transaction services могут сохранять прямые queries
по [Data](backend-data.md); use case получает данные через repository и сам
Prisma queries не выполняет. Entity/interface на каждую таблицу не требуется.
DTO остаются транспортными runtime classes; существующее использование DTO
на application boundary допустимо по [HTTP API](backend-http-api.md).
## Существующие seams
Сохраняй существующий error contract: HTTP exceptions остаются на application/
transport boundary. Чистому расчёту не нужен новый framework ошибок.
Точные ограничения — [HTTP](backend-http-api.md), [Data](backend-data.md),
[Security](security-auth.md), [Integrations](backend-integrations.md).
`TbankConnectionModule` уже объединяет счета, портфель, операции, доходы и события.
Новый связанный use case обычно добавляется в него; создание отдельного module
требует собственной ответственности, не только нового файла.
## Сценарии и размер сервисов
- [BrokerageOperationsController](../../src/tbank/brokerage-operations.controller.ts) передаёт user/account/query в service, без финансовых вычислений в controller.
- [TbankAccessService](../../src/tbank/tbank-access.service.ts) владеет connection/account access и generation; новые account use cases используют его вместо копирования guard/decrypt logic.
- [decimal.ts](../../src/tbank/decimal.ts) — пример pure domain helper; Injectable и adapter для арифметики не нужны.
Разделяй независимые операции, а не строки. `BankCardsService` отвечает за карты,
`BankOperationsService` — журнал/переводы/возвраты, `ExpenseCategoriesService` —
категории, `GetBankSummaryUseCase` — сводку. `BankCardsRepository` сохраняет
общий transaction retry и owner lookup; денежные транзакции не разрываются.
Чистые расчёты банковских карт находятся в `domain` этого модуля.
Наличие guard подтверждает session, но не владение account. При пересечении
access boundary добавь [Security](security-auth.md). Ошибка upstream не должна
обходить общий HTTP filter или превращать чужой account в доступный.
У инструментов `StockCatalogService` / `BondCatalogService` хранят идентичность
листинга и общий кэш slice; market service — котировки/свечи, income service — выплаты/личные позиции.
`GetStockCompanyUseCase` владеет сведениями об эмитенте. Ключи, TTL и pending
promises разделяют прежний экземпляр кэша; extract не должен менять generation.
Portfolio и operations сохраняют связную координацию снимков/страниц и кэшей.
Расчёты портфеля находятся в `portfolio/domain`; преобразование брокерских
позиций — в `tbank-portfolio.mapper.ts`. `bond-money.mapper.ts` преобразует
ответ клиента и не является value object.
Выбирай обычную функцию для чистого вычисления; repository contract — только
для реальной границы. CQRS/buses/events, BaseRepository/BaseService, DI token
для каждого класса, mapper/presenter для каждого DTO и новые dependencies
не нужны для структурного переноса.
## Common
`common` содержит общий error filter, logging, API decorators, тип `Money`,
его HTTP `MoneyDto` и точную decimal-арифметику, используемые несколькими областями. Код одного
модуля принадлежит этому модулю: например, RUB/kopecks rules — банковским картам,
protobuf units/nano conversion — T-Bank integration. Domain использует
`money.types.ts`, а не Swagger DTO; это общая форма денег для карт и портфеля.
Давай функциям предметные имена; не создавай глобальные `utils.ts`/`helpers.ts`.
## Logging