team rating
This commit is contained in:
606
plans/seeds-clean-arch.md
Normal file
606
plans/seeds-clean-arch.md
Normal file
@@ -0,0 +1,606 @@
|
||||
# Clean Architecture Violations & Refactoring Plan
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Recent changes introduced severe violations of Clean Architecture principles by creating singleton stores and in-memory services that bypass proper dependency injection and repository patterns. This document outlines all violations and provides a comprehensive refactoring strategy.
|
||||
|
||||
---
|
||||
|
||||
## 1. Violations Identified
|
||||
|
||||
### 1.1 Critical Violations
|
||||
|
||||
#### **Singleton Pattern in Adapters Layer**
|
||||
- **File**: `adapters/racing/services/DriverStatsStore.ts`
|
||||
- **Violation**: Global singleton with `getInstance()` static method
|
||||
- **Impact**: Bypasses dependency injection, creates hidden global state
|
||||
- **Lines**: 7-18
|
||||
|
||||
#### **In-Memory Services Using Singletons**
|
||||
- **File**: `adapters/racing/services/InMemoryRankingService.ts`
|
||||
- **Violation**: Direct singleton access via `DriverStatsStore.getInstance()` (line 14)
|
||||
- **Impact**: Service depends on global state, not injectable dependencies
|
||||
- **Lines**: 14
|
||||
|
||||
#### **In-Memory Driver Stats Service**
|
||||
- **File**: `adapters/racing/services/InMemoryDriverStatsService.ts`
|
||||
- **Violation**: Uses singleton store instead of repository pattern
|
||||
- **Impact**: Business logic depends on infrastructure implementation
|
||||
- **Lines**: 10
|
||||
|
||||
#### **Team Stats Store**
|
||||
- **File**: `adapters/racing/services/TeamStatsStore.ts`
|
||||
- **Violation**: Same singleton pattern as DriverStatsStore
|
||||
- **Impact**: Global state management in adapters layer
|
||||
|
||||
### 1.2 Architecture Violations
|
||||
|
||||
#### **Domain Services in Adapters**
|
||||
- **Location**: `adapters/racing/services/`
|
||||
- **Violation**: Services should be in domain layer, not adapters
|
||||
- **Impact**: Mixes application logic with infrastructure
|
||||
|
||||
#### **Hardcoded Data Sources**
|
||||
- **Issue**: RankingService computes from singleton store, not real data
|
||||
- **Impact**: Rankings don't reflect actual race results/standings
|
||||
|
||||
---
|
||||
|
||||
## 2. Clean Architecture Principles Violated
|
||||
|
||||
### 2.1 Dependency Rule
|
||||
**Principle**: Dependencies must point inward (domain → application → adapters → frameworks)
|
||||
|
||||
**Violation**:
|
||||
- `InMemoryRankingService` (adapters) → `DriverStatsStore` (singleton global)
|
||||
- This creates a dependency on global state, not domain abstractions
|
||||
|
||||
### 2.2 Dependency Injection
|
||||
**Principle**: All dependencies must be injected, never fetched
|
||||
|
||||
**Violation**:
|
||||
```typescript
|
||||
// ❌ WRONG
|
||||
const statsStore = DriverStatsStore.getInstance();
|
||||
|
||||
// ✅ CORRECT
|
||||
constructor(private readonly statsRepository: IDriverStatsRepository) {}
|
||||
```
|
||||
|
||||
### 2.3 Repository Pattern
|
||||
**Principle**: Persistence concerns belong in repositories, not services
|
||||
|
||||
**Violation**:
|
||||
- `DriverStatsStore` acts as a repository but is a singleton
|
||||
- Services directly access store instead of using repository interfaces
|
||||
|
||||
### 2.4 Domain Service Purity
|
||||
**Principle**: Domain services contain business logic, no persistence
|
||||
|
||||
**Violation**:
|
||||
- `InMemoryRankingService` is in adapters, not domain
|
||||
- It contains persistence logic (reading from store)
|
||||
|
||||
---
|
||||
|
||||
## 3. Proper Architecture Specification
|
||||
|
||||
### 3.1 Correct Layer Structure
|
||||
|
||||
```
|
||||
core/racing/
|
||||
├── domain/
|
||||
│ ├── services/
|
||||
│ │ ├── IRankingService.ts # Domain interface
|
||||
│ │ └── IDriverStatsService.ts # Domain interface
|
||||
│ └── repositories/
|
||||
│ ├── IResultRepository.ts # Persistence port
|
||||
│ ├── IStandingRepository.ts # Persistence port
|
||||
│ └── IDriverRepository.ts # Persistence port
|
||||
|
||||
adapters/racing/
|
||||
├── persistence/
|
||||
│ ├── inmemory/
|
||||
│ │ ├── InMemoryResultRepository.ts
|
||||
│ │ ├── InMemoryStandingRepository.ts
|
||||
│ │ └── InMemoryDriverRepository.ts
|
||||
│ └── typeorm/
|
||||
│ ├── TypeOrmResultRepository.ts
|
||||
│ ├── TypeOrmStandingRepository.ts
|
||||
│ └── TypeOrmDriverRepository.ts
|
||||
|
||||
apps/api/racing/
|
||||
├── controllers/
|
||||
├── services/
|
||||
└── presenters/
|
||||
```
|
||||
|
||||
### 3.2 Domain Services (Pure Business Logic)
|
||||
|
||||
**Location**: `core/racing/domain/services/`
|
||||
|
||||
**Characteristics**:
|
||||
- No persistence
|
||||
- No singletons
|
||||
- Pure business logic
|
||||
- Injected dependencies via interfaces
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
// core/racing/domain/services/RankingService.ts
|
||||
export class RankingService implements IRankingService {
|
||||
constructor(
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
async getAllDriverRankings(): Promise<DriverRanking[]> {
|
||||
// Query real data from repositories
|
||||
const standings = await this.standingRepository.findAll();
|
||||
const drivers = await this.driverRepository.findAll();
|
||||
|
||||
// Compute rankings from actual data
|
||||
return this.computeRankings(standings, drivers);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Repository Pattern
|
||||
|
||||
**Location**: `adapters/racing/persistence/`
|
||||
|
||||
**Characteristics**:
|
||||
- Implement domain repository interfaces
|
||||
- Handle persistence details
|
||||
- No singletons
|
||||
- Injected via constructor
|
||||
|
||||
**Example**:
|
||||
```typescript
|
||||
// adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts
|
||||
export class InMemoryStandingRepository implements IStandingRepository {
|
||||
private standings = new Map<string, Standing>();
|
||||
|
||||
async findByLeagueId(leagueId: string): Promise<Standing[]> {
|
||||
return Array.from(this.standings.values())
|
||||
.filter(s => s.leagueId === leagueId)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
}
|
||||
|
||||
async save(standing: Standing): Promise<Standing> {
|
||||
const key = `${standing.leagueId}-${standing.driverId}`;
|
||||
this.standings.set(key, standing);
|
||||
return standing;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Refactoring Strategy
|
||||
|
||||
### 4.1 Remove Violating Files
|
||||
|
||||
**DELETE**:
|
||||
- `adapters/racing/services/DriverStatsStore.ts`
|
||||
- `adapters/racing/services/TeamStatsStore.ts`
|
||||
- `adapters/racing/services/InMemoryRankingService.ts`
|
||||
- `adapters/racing/services/InMemoryDriverStatsService.ts`
|
||||
|
||||
### 4.2 Create Proper Domain Services
|
||||
|
||||
**CREATE**: `core/racing/domain/services/RankingService.ts`
|
||||
```typescript
|
||||
export class RankingService implements IRankingService {
|
||||
constructor(
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
private readonly driverRepository: IDriverRepository,
|
||||
private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
async getAllDriverRankings(): Promise<DriverRanking[]> {
|
||||
this.logger.debug('[RankingService] Computing rankings from standings');
|
||||
|
||||
const standings = await this.standingRepository.findAll();
|
||||
const drivers = await this.driverRepository.findAll();
|
||||
|
||||
// Group standings by driver and compute stats
|
||||
const driverStats = new Map<string, { rating: number; wins: number; races: number }>();
|
||||
|
||||
for (const standing of standings) {
|
||||
const existing = driverStats.get(standing.driverId) || { rating: 0, wins: 0, races: 0 };
|
||||
existing.races++;
|
||||
if (standing.position === 1) existing.wins++;
|
||||
existing.rating += this.calculateRating(standing.position);
|
||||
driverStats.set(standing.driverId, existing);
|
||||
}
|
||||
|
||||
// Convert to rankings
|
||||
const rankings: DriverRanking[] = Array.from(driverStats.entries()).map(([driverId, stats]) => ({
|
||||
driverId,
|
||||
rating: Math.round(stats.rating / stats.races),
|
||||
wins: stats.wins,
|
||||
totalRaces: stats.races,
|
||||
overallRank: null
|
||||
}));
|
||||
|
||||
// Sort by rating and assign ranks
|
||||
rankings.sort((a, b) => b.rating - a.rating);
|
||||
rankings.forEach((r, idx) => r.overallRank = idx + 1);
|
||||
|
||||
return rankings;
|
||||
}
|
||||
|
||||
private calculateRating(position: number): number {
|
||||
// iRacing-style rating calculation
|
||||
const base = 1000;
|
||||
const points = Math.max(0, 25 - position);
|
||||
return base + (points * 50);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**CREATE**: `core/racing/domain/services/DriverStatsService.ts`
|
||||
```typescript
|
||||
export class DriverStatsService implements IDriverStatsService {
|
||||
constructor(
|
||||
private readonly resultRepository: IResultRepository,
|
||||
private readonly standingRepository: IStandingRepository,
|
||||
private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
async getDriverStats(driverId: string): Promise<DriverStats | null> {
|
||||
const results = await this.resultRepository.findByDriverId(driverId);
|
||||
const standings = await this.standingRepository.findAll();
|
||||
|
||||
if (results.length === 0) return null;
|
||||
|
||||
const wins = results.filter(r => r.position === 1).length;
|
||||
const podiums = results.filter(r => r.position <= 3).length;
|
||||
const totalRaces = results.length;
|
||||
|
||||
// Calculate rating from standings
|
||||
const driverStanding = standings.find(s => s.driverId === driverId);
|
||||
const rating = driverStanding ? this.calculateRatingFromStanding(driverStanding) : 1000;
|
||||
|
||||
// Find overall rank
|
||||
const sortedStandings = standings.sort((a, b) => b.points - a.points);
|
||||
const rankIndex = sortedStandings.findIndex(s => s.driverId === driverId);
|
||||
const overallRank = rankIndex >= 0 ? rankIndex + 1 : null;
|
||||
|
||||
return {
|
||||
rating,
|
||||
wins,
|
||||
podiums,
|
||||
totalRaces,
|
||||
overallRank
|
||||
};
|
||||
}
|
||||
|
||||
private calculateRatingFromStanding(standing: Standing): number {
|
||||
// Calculate based on position and points
|
||||
return Math.round(1000 + (standing.points * 10));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Create Proper Repositories
|
||||
|
||||
**CREATE**: `adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository.ts`
|
||||
```typescript
|
||||
export class InMemoryDriverStatsRepository implements IDriverStatsRepository {
|
||||
private stats = new Map<string, DriverStats>();
|
||||
|
||||
async getDriverStats(driverId: string): Promise<DriverStats | null> {
|
||||
return this.stats.get(driverId) || null;
|
||||
}
|
||||
|
||||
async saveDriverStats(driverId: string, stats: DriverStats): Promise<void> {
|
||||
this.stats.set(driverId, stats);
|
||||
}
|
||||
|
||||
async getAllStats(): Promise<Map<string, DriverStats>> {
|
||||
return new Map(this.stats);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 Update Seed Data Strategy
|
||||
|
||||
**Current Problem**: Seeds populate singleton stores directly
|
||||
|
||||
**New Strategy**: Seed proper repositories, compute stats from results
|
||||
|
||||
**UPDATE**: `adapters/bootstrap/SeedRacingData.ts`
|
||||
```typescript
|
||||
export class SeedRacingData {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly seedDeps: RacingSeedDependencies,
|
||||
private readonly statsRepository: IDriverStatsRepository // NEW
|
||||
) {}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
// ... existing seeding logic ...
|
||||
|
||||
// After seeding results and standings, compute and store stats
|
||||
await this.computeAndStoreDriverStats();
|
||||
await this.computeAndStoreTeamStats();
|
||||
}
|
||||
|
||||
private async computeAndStoreDriverStats(): Promise<void> {
|
||||
const drivers = await this.seedDeps.driverRepository.findAll();
|
||||
const standings = await this.seedDeps.standingRepository.findAll();
|
||||
|
||||
for (const driver of drivers) {
|
||||
const driverStandings = standings.filter(s => s.driverId === driver.id);
|
||||
if (driverStandings.length === 0) continue;
|
||||
|
||||
const stats = this.calculateDriverStats(driver, driverStandings);
|
||||
await this.statsRepository.saveDriverStats(driver.id, stats);
|
||||
}
|
||||
|
||||
this.logger.info(`[Bootstrap] Computed stats for ${drivers.length} drivers`);
|
||||
}
|
||||
|
||||
private calculateDriverStats(driver: Driver, standings: Standing[]): DriverStats {
|
||||
const wins = standings.filter(s => s.position === 1).length;
|
||||
const podiums = standings.filter(s => s.position <= 3).length;
|
||||
const totalRaces = standings.length;
|
||||
const avgPosition = standings.reduce((sum, s) => sum + s.position, 0) / totalRaces;
|
||||
|
||||
// Calculate rating based on performance
|
||||
const baseRating = 1000;
|
||||
const performanceBonus = (wins * 100) + (podiums * 50) + Math.max(0, 200 - (avgPosition * 10));
|
||||
const rating = Math.round(baseRating + performanceBonus);
|
||||
|
||||
// Find overall rank
|
||||
const allStandings = await this.seedDeps.standingRepository.findAll();
|
||||
const sorted = allStandings.sort((a, b) => b.points - a.points);
|
||||
const rankIndex = sorted.findIndex(s => s.driverId === driver.id);
|
||||
const overallRank = rankIndex >= 0 ? rankIndex + 1 : null;
|
||||
|
||||
return {
|
||||
rating,
|
||||
safetyRating: 85, // Could be computed from penalties/incidents
|
||||
sportsmanshipRating: 4.5, // Could be computed from protests
|
||||
totalRaces,
|
||||
wins,
|
||||
podiums,
|
||||
dnfs: totalRaces - wins - podiums, // Approximate
|
||||
avgFinish: avgPosition,
|
||||
bestFinish: Math.min(...standings.map(s => s.position)),
|
||||
worstFinish: Math.max(...standings.map(s => s.position)),
|
||||
consistency: Math.round(100 - (avgPosition * 2)), // Simplified
|
||||
experienceLevel: this.determineExperienceLevel(totalRaces),
|
||||
overallRank
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Frontend Data Strategy
|
||||
|
||||
### 5.1 Media Repository Pattern
|
||||
|
||||
**Location**: `adapters/racing/persistence/media/`
|
||||
|
||||
**Purpose**: Handle static assets (logos, images, categories)
|
||||
|
||||
**Structure**:
|
||||
```
|
||||
adapters/racing/persistence/media/
|
||||
├── IMediaRepository.ts
|
||||
├── InMemoryMediaRepository.ts
|
||||
├── FileSystemMediaRepository.ts
|
||||
└── S3MediaRepository.ts
|
||||
```
|
||||
|
||||
**Interface**:
|
||||
```typescript
|
||||
export interface IMediaRepository {
|
||||
getDriverAvatar(driverId: string): Promise<string | null>;
|
||||
getTeamLogo(teamId: string): Promise<string | null>;
|
||||
getTrackImage(trackId: string): Promise<string | null>;
|
||||
getCategoryIcon(categoryId: string): Promise<string | null>;
|
||||
getSponsorLogo(sponsorId: string): Promise<string | null>;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Data Enrichment Strategy
|
||||
|
||||
**Problem**: Frontend needs ratings, wins, categories, logos
|
||||
|
||||
**Solution**:
|
||||
1. **Seed real data** (results, standings, races)
|
||||
2. **Compute stats** from real data
|
||||
3. **Store in repositories** (not singletons)
|
||||
4. **Serve via queries** (CQRS pattern)
|
||||
|
||||
**Flow**:
|
||||
```
|
||||
Seed Results → Compute Standings → Calculate Stats → Store in Repository → Query for Frontend
|
||||
```
|
||||
|
||||
### 5.3 Query Layer for Frontend
|
||||
|
||||
**Location**: `core/racing/application/queries/`
|
||||
|
||||
**Examples**:
|
||||
```typescript
|
||||
// GetDriverProfileQuery.ts
|
||||
export class GetDriverProfileQuery {
|
||||
constructor(
|
||||
private readonly driverStatsRepository: IDriverStatsRepository,
|
||||
private readonly mediaRepository: IMediaRepository,
|
||||
private readonly driverRepository: IDriverRepository
|
||||
) {}
|
||||
|
||||
async execute(driverId: string): Promise<DriverProfileViewModel> {
|
||||
const driver = await this.driverRepository.findById(driverId);
|
||||
const stats = await this.driverStatsRepository.getDriverStats(driverId);
|
||||
const avatar = await this.mediaRepository.getDriverAvatar(driverId);
|
||||
|
||||
return {
|
||||
id: driverId,
|
||||
name: driver.name,
|
||||
avatar,
|
||||
rating: stats.rating,
|
||||
wins: stats.wins,
|
||||
podiums: stats.podiums,
|
||||
totalRaces: stats.totalRaces,
|
||||
rank: stats.overallRank,
|
||||
experienceLevel: stats.experienceLevel
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Steps
|
||||
|
||||
### Phase 1: Remove Violations (Immediate)
|
||||
1. ✅ Delete `DriverStatsStore.ts`
|
||||
2. ✅ Delete `TeamStatsStore.ts`
|
||||
3. ✅ Delete `InMemoryRankingService.ts`
|
||||
4. ✅ Delete `InMemoryDriverStatsService.ts`
|
||||
5. ✅ Remove imports from `SeedRacingData.ts`
|
||||
|
||||
### Phase 2: Create Proper Infrastructure
|
||||
1. ✅ Create `core/racing/domain/services/RankingService.ts`
|
||||
2. ✅ Create `core/racing/domain/services/DriverStatsService.ts`
|
||||
3. ✅ Create `adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository.ts`
|
||||
4. ✅ Create `adapters/racing/persistence/media/InMemoryMediaRepository.ts`
|
||||
|
||||
### Phase 3: Update Seed Logic
|
||||
1. ✅ Modify `SeedRacingData.ts` to compute stats from results
|
||||
2. ✅ Remove singleton store population
|
||||
3. ✅ Add stats repository injection
|
||||
4. ✅ Add media data seeding
|
||||
|
||||
### Phase 4: Update Application Layer
|
||||
1. ✅ Update factories to inject proper services
|
||||
2. ✅ Update controllers to use domain services
|
||||
3. ✅ Update presenters to query repositories
|
||||
|
||||
### Phase 5: Frontend Integration
|
||||
1. ✅ Create query use cases for frontend data
|
||||
2. ✅ Implement media repository for assets
|
||||
3. ✅ Update API endpoints to serve computed data
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
### 7.1 Unit Tests
|
||||
```typescript
|
||||
// RankingService.test.ts
|
||||
describe('RankingService', () => {
|
||||
it('computes rankings from real standings', async () => {
|
||||
const mockStandings = [/* real standings */];
|
||||
const mockResults = [/* real results */];
|
||||
|
||||
const service = new RankingService(
|
||||
mockResultRepo,
|
||||
mockStandingRepo,
|
||||
mockDriverRepo,
|
||||
mockLogger
|
||||
);
|
||||
|
||||
const rankings = await service.getAllDriverRankings();
|
||||
|
||||
expect(rankings).toHaveLength(150);
|
||||
expect(rankings[0].overallRank).toBe(1);
|
||||
expect(rankings[0].rating).toBeGreaterThan(1000);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 7.2 Integration Tests
|
||||
```typescript
|
||||
// SeedRacingData.integration.test.ts
|
||||
describe('SeedRacingData', () => {
|
||||
it('seeds data and computes stats correctly', async () => {
|
||||
const seed = new SeedRacingData(logger, deps, statsRepo);
|
||||
await seed.execute();
|
||||
|
||||
const stats = await statsRepo.getDriverStats(driverId);
|
||||
expect(stats.rating).toBeDefined();
|
||||
expect(stats.wins).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Benefits of This Approach
|
||||
|
||||
### 8.1 Architecture Benefits
|
||||
- ✅ **Clean Architecture Compliance**: Proper layer separation
|
||||
- ✅ **Dependency Injection**: All dependencies injected
|
||||
- ✅ **Testability**: Easy to mock repositories
|
||||
- ✅ **Maintainability**: Clear separation of concerns
|
||||
|
||||
### 8.2 Functional Benefits
|
||||
- ✅ **Real Data**: Rankings computed from actual race results
|
||||
- ✅ **Scalability**: Works with any persistence (memory, Postgres, etc.)
|
||||
- ✅ **Flexibility**: Easy to add new data sources
|
||||
- ✅ **Consistency**: Single source of truth for stats
|
||||
|
||||
### 8.3 Development Benefits
|
||||
- ✅ **No Hidden State**: No singletons
|
||||
- ✅ **Explicit Dependencies**: Clear what each service needs
|
||||
- ✅ **Framework Agnostic**: Core doesn't depend on infrastructure
|
||||
- ✅ **Future Proof**: Easy to migrate to different storage
|
||||
|
||||
---
|
||||
|
||||
## 9. Migration Checklist
|
||||
|
||||
- [ ] Remove all singleton stores
|
||||
- [ ] Remove all in-memory services from adapters/racing/services/
|
||||
- [ ] Create proper domain services in core/racing/domain/services/
|
||||
- [ ] Create proper repositories in adapters/racing/persistence/
|
||||
- [ ] Update SeedRacingData to compute stats from real data
|
||||
- [ ] Update all factories to use dependency injection
|
||||
- [ ] Update controllers to use domain services
|
||||
- [ ] Update presenters to use query patterns
|
||||
- [ ] Add media repository for frontend assets
|
||||
- [ ] Create query use cases for frontend data
|
||||
- [ ] Update tests to use proper patterns
|
||||
- [ ] Verify no singleton usage anywhere
|
||||
- [ ] Verify all services are pure domain services
|
||||
- [ ] Verify all persistence is in repositories
|
||||
|
||||
---
|
||||
|
||||
## 10. Summary
|
||||
|
||||
The current implementation violates Clean Architecture by:
|
||||
1. Using singletons for state management
|
||||
2. Placing services in adapters layer
|
||||
3. Hardcoding data sources instead of using repositories
|
||||
4. Mixing persistence logic with business logic
|
||||
|
||||
The solution requires:
|
||||
1. **Removing** all singleton stores and in-memory services
|
||||
2. **Creating** proper domain services that compute from real data
|
||||
3. **Implementing** repository pattern for all persistence
|
||||
4. **Updating** seed logic to compute stats from results/standings
|
||||
5. **Adding** media repository for frontend assets
|
||||
6. **Using** CQRS pattern for queries
|
||||
|
||||
This will result in a clean, maintainable, and scalable architecture that properly follows Clean Architecture principles.
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0
|
||||
**Last Updated**: 2025-12-29
|
||||
**Status**: Planning Phase
|
||||
**Next Steps**: Implementation
|
||||
Reference in New Issue
Block a user