From c4001fe5d27428547511b93a2030e9421c75412f Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 15 Dec 2025 15:28:10 +0100 Subject: [PATCH] wip --- ...T15:42:00Z_clean-architecture-migration.md | 641 ++++++++++++++---- 1 file changed, 496 insertions(+), 145 deletions(-) diff --git a/plans/2025-12-15T15:42:00Z_clean-architecture-migration.md b/plans/2025-12-15T15:42:00Z_clean-architecture-migration.md index ca2dd2470..ac1883d2a 100644 --- a/plans/2025-12-15T15:42:00Z_clean-architecture-migration.md +++ b/plans/2025-12-15T15:42:00Z_clean-architecture-migration.md @@ -1,22 +1,24 @@ -Clean Architecture plan for GridPilot (NestJS-focused) +# Clean Architecture plan for GridPilot (NestJS‑focused) -This document defines the target architecture and rules for GridPilot. -It is written as a developer-facing contract: what goes where, what is allowed, and what is forbidden. +This document defines the **target architecture** and **rules** for GridPilot. +It is written as a **developer-facing contract**: what goes where, what is allowed, and what is forbidden. -⸻ +--- -1. Architectural goals - • Strict Clean Architecture (dependency rule enforced) - • Domain-first design (DDD-inspired) - • Frameworks are delivery mechanisms, not architecture - • Business logic is isolated from persistence, UI, and infrastructure - • Explicit composition roots - • High testability without mocking the domain +## 1. Architectural goals -⸻ +* Strict Clean Architecture (dependency rule enforced) +* Domain-first design (DDD-inspired) +* Frameworks are delivery mechanisms, not architecture +* Business logic is isolated from persistence, UI, and infrastructure +* Explicit composition roots +* High testability without mocking the domain -2. High-level structure +--- +## 2. High-level structure + +``` /apps /api # NestJS API (delivery mechanism) /website # Next.js website (delivery mechanism) @@ -47,232 +49,581 @@ It is written as a developer-facing contract: what goes where, what is allowed, /builders /fakes /fixtures +``` +--- -⸻ +## 3. Dependency rule (non-negotiable) -3. Dependency rule (non-negotiable) - -Dependencies must only point inward: +Dependencies **must only point inward**: +``` apps → adapters → core +``` Forbidden: - • core importing from adapters - • core importing from apps - • domain entities importing ORM, NestJS, or framework code -⸻ +* `core` importing from `adapters` +* `core` importing from `apps` +* domain entities importing ORM, NestJS, or framework code -4. Core rules +--- -The Core contains only business rules. +## 4. Core rules -Core MAY contain: - • Domain entities - • Value objects - • Domain services - • Domain events - • Repository interfaces - • Application use cases - • Application-level ports +The Core contains **only business rules**. -Core MUST NOT contain: - • ORM entities - • Persistence implementations - • In-memory repositories - • NestJS decorators - • TypeORM decorators - • HTTP / GraphQL / IPC concerns - • Faker, demo data, seeds +### Core MAY contain: -⸻ +* Domain entities +* Value objects +* Domain services +* Domain events +* Repository interfaces +* Application use cases +* Application-level ports -5. Domain entities +### Core MUST NOT contain: + +* ORM entities +* Persistence implementations +* In-memory repositories +* NestJS decorators +* TypeORM decorators +* HTTP / GraphQL / IPC concerns +* Faker, demo data, seeds + +--- + +## 5. Domain entities Domain entities: - • Represent business concepts - • Enforce invariants - • Contain behavior - • Are immutable or controlled via methods + +* Represent business concepts +* Enforce invariants +* Contain behavior +* Are immutable or controlled via methods Example characteristics: - • Private constructors - • Static factory methods - • Explicit validation - • Value objects for identity -Domain entities must never: - • Be decorated with @Entity, @Column, etc. - • Be reused as persistence models - • Know how they are stored +* Private constructors +* Static factory methods +* Explicit validation +* Value objects for identity -⸻ +Domain entities **must never**: -6. Persistence entities (ORM) +* Be decorated with `@Entity`, `@Column`, etc. +* Be reused as persistence models +* Know how they are stored -Persistence entities live in adapters and are data-only. +--- +## 6. Persistence entities (ORM) + +Persistence entities live in adapters and are **data-only**. + +``` /adapters/persistence/typeorm/ - PageViewOrmEntity.ts +``` Rules: - • No business logic - • No validation - • No behavior - • Flat data structures -ORM entities are not domain entities. +* No business logic +* No validation +* No behavior +* Flat data structures -⸻ +ORM entities are **not domain entities**. -7. Mapping (anti-corruption layer) +--- + +## 7. Mapping (anti-corruption layer) Mapping between domain and persistence is explicit and isolated. +``` /adapters/persistence/typeorm/ - PageViewMapper.ts +``` Rules: - • Domain ↔ ORM mapping only happens in adapters - • Mappers are pure functions - • Boilerplate is acceptable and expected -⸻ +* Domain ↔ ORM mapping only happens in adapters +* Mappers are pure functions +* Boilerplate is acceptable and expected -8. Repositories +--- -Core +## 8. Repositories +### Core + +``` /core//domain/repositories - IPageViewRepository.ts +``` Only interfaces. -Adapters +### Adapters +``` /adapters/persistence/typeorm/ - PageViewTypeOrmRepository.ts /adapters/persistence/inmemory/ - InMemoryPageViewRepository.ts +``` Rules: - • Repositories translate between domain entities and storage models - • Repositories implement core interfaces - • Repositories hide all persistence details from the core -⸻ +* Repositories translate between domain entities and storage models +* Repositories implement core interfaces +* Repositories hide all persistence details from the core -9. In-memory repositories +--- -In-memory repositories are test adapters, not core infrastructure. +## 9. In-memory repositories + +In-memory repositories are **test adapters**, not core infrastructure. Rules: - • Never placed in /core - • Allowed only in /adapters/persistence/inmemory - • Used for unit tests and integration tests - • Must behave like real repositories -⸻ +* Never placed in `/core` +* Allowed only in `/adapters/persistence/inmemory` +* Used for unit tests and integration tests +* Must behave like real repositories -10. NestJS API (/apps/api) +--- -The NestJS API is a delivery mechanism. +## 10. NestJS API (`/apps/api`) -Responsibilities: - • Controllers - • DTOs - • Request validation - • Dependency injection - • Adapter selection (prod vs test) +The NestJS API is a **delivery mechanism**. -Forbidden: - • Business logic - • Domain rules - • Persistence logic +### Responsibilities: -NestJS modules are composition roots. +* Controllers +* DTOs +* Request validation +* Dependency injection +* Adapter selection (prod vs test) -⸻ +### Forbidden: -11. Dependency injection +* Business logic +* Domain rules +* Persistence logic -DI is configured only in apps. +NestJS modules are **composition roots**. + +--- + +## 11. Dependency injection + +DI is configured **only in apps**. Example: +``` provide: IPageViewRepository useClass: PageViewTypeOrmRepository +``` -Switching implementations (e.g. InMemory vs TypeORM) happens outside the core. +Switching implementations (e.g. InMemory vs TypeORM) happens **outside the core**. -⸻ +--- -12. Testing strategy +## 12. Testing strategy -Domain tests - • Test entities and value objects directly - • No adapters - • No frameworks +Tests are **clients of the system**, not part of the architecture. -Use case tests - • Core + in-memory adapters - • No NestJS +### Core principles -API tests - • NestJS TestingModule - • Explicit adapter overrides +* `/tests` contains test cases +* `/testing` contains **test support only** (helpers, factories, fixtures) +* Production code must never import from `/testing` -⸻ +--- -13. Testing helpers +## 13. Test support structure (`/testing`) -Testing helpers live in /testing. +The `/testing` folder is a **test-support library**, not a layer. +It follows a single, strict structure: -Contexts - • One context per bounded context - • Provide repositories + use cases +``` +/testing + /fixtures # Static reference data (NO logic, NO faker) + /factories # Domain object factories (faker allowed ONLY here) + /fakes # Fake implementations of external ports + /helpers # Generic test utilities (time, ids, assertions) + index.ts +``` -Factories - • Create valid domain objects - • Express intent, not randomness +--- -Builders - • Build complex aggregates fluently +## 14. Rules for test support -Fakes - • Replace external systems (payments, email, etc.) +The project uses **factories only**. +There is **no separate concept of fixtures**. -⸻ +All test data is produced via factories. +This avoids duplication, drift, and ambiguity. -14. Read models and projections +--- -Not every query needs a domain entity. +### Factories (the single source of test data) + +Factories are the **only approved way** to create test data. Rules: - • Reports, analytics, dashboards may use DTOs or projections - • No forced mapping into domain entities - • Domain entities are for behavior, not reporting -⸻ +* Factories create **valid domain objects** or **valid DTOs** (boundary only) +* One factory per concept +* Stateless +* One export per file +* File name === exported symbol +* Faker / randomness allowed here -15. Golden rules - • Domain entities never depend on infrastructure - • ORM entities never contain behavior - • Repositories are anti-corruption layers - • Adapters are replaceable - • Apps wire everything together - • Tests never leak into production code +Factories must **NOT**: -⸻ +* Use repositories +* Perform IO +* Contain assertions +* Contain domain logic -16. Summary +--- -This architecture ensures: - • Long-term maintainability - • Framework independence - • Safe refactoring - • Clear ownership of responsibilities +### Fakes -If a class violates a rule, it is in the wrong layer. \ No newline at end of file +Fakes replace **external systems only**. + +Rules: + +* Implement application ports +* Explicit behavior +* One export per file +* No domain logic + +--- + +### Helpers + +Helpers are **pure utilities**. + +Rules: + +* No domain knowledge +* No adapters +* No randomness + +--- + +### Factories + +* Create **valid domain objects** +* Stateless +* Deterministic unless randomness is required +* Faker is allowed **only here** + +Factories **must NOT**: + +* Use repositories +* Use DTOs +* Perform IO + +--- + +### Fakes + +* Implement application ports +* Replace external systems (payments, email, auth, etc.) +* Explicit, controllable behavior + +--- + +### Helpers + +* Pure utilities +* No domain logic +* No adapters + +--- + +## 15. DTO usage in tests (strict rule) + +DTOs are **boundary objects**, not domain concepts. + +Rules: + +* Domain tests: **never** use DTOs +* Domain factories: **never** use DTOs +* Use case tests: DTOs allowed **only if the use case explicitly accepts a DTO** +* API / controller tests: DTOs are expected and allowed + +If DTOs are needed in tests, they must be created explicitly, e.g.: + +``` +/testing/dto-factories +``` + +This makes boundary tests obvious and prevents accidental coupling. + +--- + +## 16. Concrete pseudo examples (authoritative) + +The following examples show the **only intended usage**, aligned with **Screaming Architecture** and **one-export-per-file**. + +Naming and placement are part of the rule. + +--- + +### Naming rules (strict) + +* One file = one export +* File name === exported symbol +* PascalCase only +* No suffixes like `.factory`, `.fixture`, `.fake` + +Correct: + +``` +PageViewFactory.ts +PageViewFixture.ts +FakePaymentGateway.ts +``` + +--- + +### Example: Domain entity + +```ts +// core/analytics/domain/entities/PageView.ts +export class PageView { + static create(props) {} + isMeaningfulView(): boolean {} +} +``` + +--- + +### Example: Fixture (static data) + +```ts +// testing/analytics/fixtures/PageViewFixture.ts +export const PageViewFixture = { + id: 'pv-1', + entityType: 'league', + entityId: 'l-1', + visitorType: 'guest', + sessionId: 's-1', +}; +``` + +Rules demonstrated: + +* Plain object +* One export +* No logic +* No faker + +--- + +### Example: Factory (domain object creation) + +```ts +// testing/analytics/factories/PageViewFactory.ts +export class PageViewFactory { + static create(overrides = {}) { + return PageView.create({ + id: randomId(), + entityType: 'league', + entityId: randomId(), + visitorType: 'guest', + sessionId: randomId(), + ...overrides, + }); + } +} +``` + +Rules demonstrated: + +* Produces domain entity +* Faker / randomness allowed here +* One export per file +* No DTOs +* No repositories + +--- + +### Example: Fake (external port) + +```ts +// testing/racing/fakes/FakePaymentGateway.ts +export class FakePaymentGateway implements PaymentGatewayPort { + charge() { + return { success: true }; + } +} +``` + +Rules demonstrated: + +* Implements a port +* Simulates external system +* One export per file +* No domain logic + +--- + +### Example: Domain test + +```ts +// tests/analytics/PageView.spec.ts +const pageView = PageViewFactory.create({ durationMs: 6000 }); + +expect(pageView.isMeaningfulView()).toBe(true); +``` + +Rules demonstrated: + +* Talks domain language +* No DTOs +* No framework + +--- + +### Example: Use case test (DTO allowed only if required) + +```ts +// tests/analytics/RecordPageView.spec.ts +const command = RecordPageViewCommandDTO.create({ ... }); + +useCase.execute(command); +``` + +Rules demonstrated: + +* DTO only used because it is the explicit input +* Boundary is under test + +--- + +### Example: API test + +```ts +// tests/api/analytics.e2e.spec.ts +POST /api/page-views +body = RecordPageViewCommandDTO +``` + +Rules demonstrated: + +* DTOs expected +* HTTP boundary +* Full stack + +--- + +## 17. Migration vs Bootstrap (production data) + +Certain data exists **not because of business rules**, but because the application must be operable. +This is **not a testing concern** and **not a domain concern**. + +--- + +### Migrations + +**Purpose:** + +* Create or evolve the database schema + +Characteristics: + +* Tables, columns, indices, constraints +* Versioned +* Deterministic +* Idempotent per version + +Rules: + +* No domain logic +* No factories +* No faker +* No test helpers +* No business decisions + +Location: + +``` +/adapters/persistence/migrations +``` + +--- + +### Bootstrap (initial application data) + +**Purpose:** + +* Ensure the system is usable after first start + +Examples: + +* Initial admin user +* System roles +* Required system configuration entries + +Characteristics: + +* Runs at application startup +* Idempotent +* Environment-driven + +Rules: + +* Must NOT live in core +* Must NOT live in testing +* Must NOT live in repositories +* Must use application use cases + +Location: + +``` +/adapters/bootstrap +``` + +Example (pseudo): + +```ts +class EnsureInitialAdminUser { + run(): Promise {} +} +``` + +The application (e.g. NestJS) decides **when** this runs. +Adapters decide **how** it runs. + +--- + +### Hard separation + +| Concern | Purpose | Location | +| --------- | ------------------- | ------------------------------- | +| Migration | Schema | adapters/persistence/migrations | +| Bootstrap | Required start data | adapters/bootstrap | +| Factory | Test data | testing | + +--- + +## 18. Final enforcement rules + +* Domain knows nothing about migrations or bootstrap +* Repositories never auto-create data +* Factories are test-only +* Bootstrap is explicit and idempotent +* Migrations and bootstrap are never mixed + +This section is authoritative.