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

11 KiB
Raw Blame History

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

// core/analytics/domain/entities/PageView.ts
export class PageView {
  static create(props) {}
  isMeaningfulView(): boolean {}
}

Example: Fixture (static data)

// 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)

// 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)

// 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

// 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)

// 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

// 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):

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.