630 lines
11 KiB
Markdown
630 lines
11 KiB
Markdown
# 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.
|
||
|
||
---
|
||
|
||
## 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.
|