This commit is contained in:
2025-12-15 15:28:10 +01:00
parent bc759a7d36
commit c4001fe5d2

View File

@@ -1,22 +1,24 @@
Clean Architecture plan for GridPilot (NestJS-focused) # Clean Architecture plan for GridPilot (NestJSfocused)
This document defines the target architecture and rules for GridPilot. 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. It is written as a **developer-facing contract**: what goes where, what is allowed, and what is forbidden.
---
1. Architectural goals ## 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
* 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 /apps
/api # NestJS API (delivery mechanism) /api # NestJS API (delivery mechanism)
/website # Next.js website (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 /builders
/fakes /fakes
/fixtures /fixtures
```
---
## 3. Dependency rule (non-negotiable)
3. Dependency rule (non-negotiable) Dependencies **must only point inward**:
Dependencies must only point inward:
```
apps → adapters → core apps → adapters → core
```
Forbidden: 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: The Core contains **only business rules**.
• Domain entities
• Value objects
• Domain services
• Domain events
• Repository interfaces
• Application use cases
• Application-level ports
Core MUST NOT contain: ### Core MAY contain:
• ORM entities
• Persistence implementations
• In-memory repositories
• NestJS decorators
• TypeORM decorators
• HTTP / GraphQL / IPC concerns
• Faker, demo data, seeds
* 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: Domain entities:
• Represent business concepts
• Enforce invariants * Represent business concepts
• Contain behavior * Enforce invariants
• Are immutable or controlled via methods * Contain behavior
* Are immutable or controlled via methods
Example characteristics: Example characteristics:
• Private constructors
• Static factory methods
• Explicit validation
• Value objects for identity
Domain entities must never: * Private constructors
• Be decorated with @Entity, @Column, etc. * Static factory methods
• Be reused as persistence models * Explicit validation
• Know how they are stored * 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/<context> /adapters/persistence/typeorm/<context>
- PageViewOrmEntity.ts - PageViewOrmEntity.ts
```
Rules: 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. Mapping between domain and persistence is explicit and isolated.
```
/adapters/persistence/typeorm/<context> /adapters/persistence/typeorm/<context>
- PageViewMapper.ts - PageViewMapper.ts
```
Rules: 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/<context>/domain/repositories /core/<context>/domain/repositories
- IPageViewRepository.ts - IPageViewRepository.ts
```
Only interfaces. Only interfaces.
Adapters ### Adapters
```
/adapters/persistence/typeorm/<context> /adapters/persistence/typeorm/<context>
- PageViewTypeOrmRepository.ts - PageViewTypeOrmRepository.ts
/adapters/persistence/inmemory/<context> /adapters/persistence/inmemory/<context>
- InMemoryPageViewRepository.ts - InMemoryPageViewRepository.ts
```
Rules: 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: 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: The NestJS API is a **delivery mechanism**.
• Controllers
• DTOs
• Request validation
• Dependency injection
• Adapter selection (prod vs test)
Forbidden: ### Responsibilities:
• Business logic
• Domain rules
• Persistence logic
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: Example:
```
provide: IPageViewRepository provide: IPageViewRepository
useClass: PageViewTypeOrmRepository 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 Tests are **clients of the system**, not part of the architecture.
• Test entities and value objects directly
• No adapters
• No frameworks
Use case tests ### Core principles
• Core + in-memory adapters
• No NestJS
API tests * `/tests` contains test cases
• NestJS TestingModule * `/testing` contains **test support only** (helpers, factories, fixtures)
• Explicit adapter overrides * 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 /testing
• Provide repositories + use cases /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 ## 14. Rules for test support
• Build complex aggregates fluently
Fakes The project uses **factories only**.
• Replace external systems (payments, email, etc.) 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: 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 Factories must **NOT**:
• 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
* Use repositories
* Perform IO
* Contain assertions
* Contain domain logic
16. Summary ---
This architecture ensures: ### Fakes
• Long-term maintainability
• Framework independence
• Safe refactoring
• Clear ownership of responsibilities
If a class violates a rule, it is in the wrong layer. 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.