Files
gridpilot.gg/plans/2025-12-15T15:42:00Z_clean-architecture-migration.md
2025-12-15 15:28:10 +01:00

630 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Clean Architecture plan for GridPilot (NestJSfocused)
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
---
## 2. High-level structure
```
/apps
/api # NestJS API (delivery mechanism)
/website # Next.js website (delivery mechanism)
/electron # Electron app (delivery mechanism)
/core # Business rules (framework-agnostic)
/analytics
/automation
/identity
/media
/notifications
/racing
/social
/shared
/adapters # Technical implementations (details)
/persistence
/typeorm
/inmemory
/auth
/media
/notifications
/logging
/testing # Test-only code (never shipped)
/contexts
/factories
/builders
/fakes
/fixtures
```
---
## 3. Dependency rule (non-negotiable)
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
---
## 4. Core rules
The Core contains **only business rules**.
### Core MAY contain:
* Domain entities
* Value objects
* Domain services
* Domain events
* Repository interfaces
* Application use cases
* Application-level ports
### 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
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
---
## 6. Persistence entities (ORM)
Persistence entities live in adapters and are **data-only**.
```
/adapters/persistence/typeorm/<context>
- PageViewOrmEntity.ts
```
Rules:
* No business logic
* No validation
* No behavior
* Flat data structures
ORM entities are **not domain entities**.
---
## 7. Mapping (anti-corruption layer)
Mapping between domain and persistence is explicit and isolated.
```
/adapters/persistence/typeorm/<context>
- PageViewMapper.ts
```
Rules:
* Domain ↔ ORM mapping only happens in adapters
* Mappers are pure functions
* Boilerplate is acceptable and expected
---
## 8. Repositories
### Core
```
/core/<context>/domain/repositories
- IPageViewRepository.ts
```
Only interfaces.
### Adapters
```
/adapters/persistence/typeorm/<context>
- PageViewTypeOrmRepository.ts
/adapters/persistence/inmemory/<context>
- InMemoryPageViewRepository.ts
```
Rules:
* 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.
Rules:
* 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**.
### Responsibilities:
* Controllers
* DTOs
* Request validation
* Dependency injection
* Adapter selection (prod vs test)
### Forbidden:
* Business logic
* Domain rules
* Persistence logic
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**.
---
## 12. Testing strategy
Tests are **clients of the system**, not part of the architecture.
### Core principles
* `/tests` contains test cases
* `/testing` contains **test support only** (helpers, factories, fixtures)
* Production code must never import from `/testing`
---
## 13. Test support structure (`/testing`)
The `/testing` folder is a **test-support library**, not a layer.
It follows a single, strict structure:
```
/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
```
---
## 14. Rules for test support
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.
---
### Factories (the single source of test data)
Factories are the **only approved way** to create test data.
Rules:
* 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
Factories must **NOT**:
* Use repositories
* Perform IO
* Contain assertions
* Contain domain logic
---
### Fakes
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<void> {}
}
```
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.