🏗️ Architecture
🎯 Overview
What is Architecture?
Architecture in software development refers to the fundamental structure of a system - how components are organized, how they communicate, and how they work together to achieve the system's goals. For LIPAIX, we follow Clean Architecture principles with practical simplifications.
Why Clean Architecture?
- 🧹 Separation of Concerns - Clear boundaries between different parts of the system
- 🔄 Dependency Inversion - High-level modules don't depend on low-level modules
- 🧪 Testability - Easy to test individual components in isolation
- 📚 Maintainability - Clear structure makes code easier to understand and modify
- 🚀 Scalability - Architecture supports growth and new features
🏢 Monorepo Structure
High-Level Organization
graph TB
subgraph "LIPAIX Monorepo"
APPS[apps/]
SHARED[shared/]
DOCS[docs/]
ROOT[Root Config]
end
subgraph "Applications"
WEB[web/ - Next.js App]
ADMIN[admin/ - PayloadCMS Admin]
DISCORD[discord-bot/ - Discord Bot]
end
subgraph "Shared Resources"
COMMON[common/ - Core Utilities]
TYPES[Type Definitions]
SERVICES[Shared Services]
end
subgraph "Documentation"
VITEPRESS[VitePress Docs]
README[README Files]
GUIDES[Development Guides]
end
ROOT --> APPS
ROOT --> SHARED
ROOT --> DOCS
APPS --> WEB
APPS --> ADMIN
APPS --> DISCORD
SHARED --> COMMON
SHARED --> TYPES
SHARED --> SERVICES
DOCS --> VITEPRESS
DOCS --> README
DOCS --> GUIDESDirectory Structure
graph LR
subgraph "Root Level"
PKG[package.json]
WORKSPACE[pnpm-workspace.yaml]
TS[tsconfig.json]
GIT[.gitignore]
end
subgraph "apps/"
WEB_DIR[web/]
ADMIN_DIR[admin/]
DISCORD_DIR[discord-bot/]
end
subgraph "shared/"
COMMON_DIR[common/]
end
subgraph "docs/"
VITEPRESS_DIR[vitepress/]
end
PKG --> WORKSPACE
WORKSPACE --> WEB_DIR
WORKSPACE --> ADMIN_DIR
WORKSPACE --> DISCORD_DIR
WORKSPACE --> COMMON_DIR
WORKSPACE --> VITEPRESS_DIR🧩 Clean Architecture Principles
Layered Architecture
graph TB
subgraph "Presentation Layer"
UI[UI Components]
PAGES[Pages/Routes]
FORMS[Forms & Validation]
end
subgraph "Application Layer"
USECASES[Use Cases]
CONTROLLERS[Controllers]
MIDDLEWARE[Middleware]
end
subgraph "Domain Layer"
ENTITIES[Entities]
VALUE_OBJECTS[Value Objects]
DOMAIN_SERVICES[Domain Services]
end
subgraph "Infrastructure Layer"
REPOSITORIES[Repositories]
EXTERNAL_APIS[External APIs]
DATABASE[Database]
end
UI --> PAGES
PAGES --> FORMS
FORMS --> USECASES
USECASES --> CONTROLLERS
CONTROLLERS --> MIDDLEWARE
MIDDLEWARE --> ENTITIES
ENTITIES --> VALUE_OBJECTS
VALUE_OBJECTS --> DOMAIN_SERVICES
DOMAIN_SERVICES --> REPOSITORIES
REPOSITORIES --> EXTERNAL_APIS
EXTERNAL_APIS --> DATABASEDependency Flow
flowchart TD
A[Presentation Layer] --> B[Application Layer]
B --> C[Domain Layer]
C --> D[Infrastructure Layer]
E[External Dependencies] --> D
F[Database] --> D
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#e8f5e8
style D fill:#fff3e0🔌 Dependency Injection
Global Reference Pattern
Instead of complex DI containers, we use a simplified Global Reference pattern:
graph LR
subgraph "GlobalRef Pattern"
GLOBAL[GlobalRef]
INSTANCES[Instances]
SINGLETONS[Singletons]
end
subgraph "Usage"
COMPONENT[Component]
SERVICE[Service]
USECASE[Use Case]
end
GLOBAL --> INSTANCES
GLOBAL --> SINGLETONS
COMPONENT --> GLOBAL
SERVICE --> GLOBAL
USECASE --> GLOBALImplementation Example
export class GlobalRef<T> {
private static instances = new Map<string, any>()
constructor(private key: string) {}
get(): T {
if (!GlobalRef.instances.has(this.key)) {
throw new Error(`No instance found for key: ${this.key}`)
}
return GlobalRef.instances.get(this.key)
}
set(instance: T): void {
GlobalRef.instances.set(this.key, instance)
}
has(): boolean {
return GlobalRef.instances.has(this.key)
}
clear(): void {
GlobalRef.instances.delete(this.key)
}
}
// Usage example
export const eventRepositoryRef = new GlobalRef<EventRepository>('EventRepository')
export const playerRepositoryRef = new GlobalRef<PlayerRepository>('PlayerRepository')🗄️ Repository Pattern
Data Access Abstraction
graph TB
subgraph "Repository Interface"
REPO_INTERFACE[Repository Interface]
METHODS[CRUD Methods]
QUERIES[Query Methods]
end
subgraph "Implementations"
MYSQL_REPO[MySQL Repository]
PAYLOAD_REPO[PayloadCMS Repository]
MOCK_REPO[Mock Repository]
end
subgraph "Usage"
USECASE[Use Case]
SERVICE[Service]
CONTROLLER[Controller]
end
REPO_INTERFACE --> METHODS
REPO_INTERFACE --> QUERIES
METHODS --> MYSQL_REPO
METHODS --> PAYLOAD_REPO
METHODS --> MOCK_REPO
USECASE --> REPO_INTERFACE
SERVICE --> REPO_INTERFACE
CONTROLLER --> REPO_INTERFACERepository Implementation
export interface EventRepository {
findById(id: string): Promise<Event | null>
findByDateRange(start: Date, end: Date): Promise<Event[]>
save(event: Event): Promise<Event>
delete(id: string): Promise<boolean>
findUpcoming(limit?: number): Promise<Event[]>
}
export class MySQLEventRepository implements EventRepository {
constructor(private mysqlService: MySQLService) {}
async findById(id: string): Promise<Event | null> {
const query = 'SELECT * FROM events WHERE id = ?'
const [rows] = await this.mysqlService.execute(query, [id])
return rows.length > 0 ? this.adaptToEntity(rows[0]) : null
}
async findByDateRange(start: Date, end: Date): Promise<Event[]> {
const query = 'SELECT * FROM events WHERE date BETWEEN ? AND ? ORDER BY date'
const [rows] = await this.mysqlService.execute(query, [start, end])
return rows.map(row => this.adaptToEntity(row))
}
// ... other methods
}🎯 Use Case Pattern
Business Logic Encapsulation
graph TD
subgraph "Use Case"
INPUT[Input Data]
VALIDATION[Validation]
BUSINESS_LOGIC[Business Logic]
OUTPUT[Output Data]
end
subgraph "Dependencies"
REPOSITORIES[Repositories]
SERVICES[Services]
VALIDATORS[Validators]
end
subgraph "Flow"
VALIDATE[Validate Input]
PROCESS[Process Business Logic]
PERSIST[Persist Changes]
RETURN[Return Result]
end
INPUT --> VALIDATION
VALIDATION --> BUSINESS_LOGIC
BUSINESS_LOGIC --> OUTPUT
VALIDATION --> VALIDATORS
BUSINESS_LOGIC --> REPOSITORIES
BUSINESS_LOGIC --> SERVICES
VALIDATE --> PROCESS
PROCESS --> PERSIST
PERSIST --> RETURNUse Case Example
export class GetNextFewEventsCommandUseCase {
constructor(
private eventRepository: EventRepository,
private dateService: DateService
) {}
async execute(command: GetNextFewEventsCommand): Promise<GetNextFewEventsResult> {
try {
// Validate input
if (command.count < 1 || command.count > 50) {
return {
success: false,
error: 'Count must be between 1 and 50'
}
}
// Get current date
const now = this.dateService.now()
// Find upcoming events
const events = await this.eventRepository.findUpcoming(command.count)
// Filter and sort
const upcomingEvents = events
.filter(event => event.date > now)
.sort((a, b) => a.date.getTime() - b.date.getTime())
.slice(0, command.count)
return {
success: true,
events: upcomingEvents
}
} catch (error) {
return {
success: false,
error: error.message
}
}
}
}🔄 Data Flow
Request Processing Flow
sequenceDiagram
participant C as Client
participant P as Pages
participant UC as Use Cases
participant R as Repositories
participant DB as Database
C->>P: HTTP Request
P->>UC: Execute Use Case
UC->>R: Query Data
R->>DB: SQL Query
DB-->>R: Data Result
R-->>UC: Domain Objects
UC-->>P: Business Result
P-->>C: HTTP ResponseComponent Communication
graph LR
subgraph "Frontend"
PAGE[Page Component]
COMPONENT[UI Component]
HOOK[Custom Hook]
end
subgraph "Backend"
API[API Route]
USECASE[Use Case]
REPOSITORY[Repository]
end
subgraph "Data"
DATABASE[(Database)]
CACHE[Cache Layer]
EXTERNAL[External APIs]
end
PAGE --> COMPONENT
COMPONENT --> HOOK
HOOK --> API
API --> USECASE
USECASE --> REPOSITORY
REPOSITORY --> DATABASE
REPOSITORY --> CACHE
REPOSITORY --> EXTERNAL📦 Package Architecture
Module Dependencies
graph TD
subgraph "Core Layer"
CORE[Core]
VALUE_OBJECTS[Value Objects]
ENTITIES[Entities]
end
subgraph "Feature Layer"
EVENTS[Events Feature]
PLAYERS[Players Feature]
AVAILABILITIES[Availabilities Feature]
end
subgraph "Infrastructure Layer"
MYSQL[MySQL Services]
PAYLOAD[PayloadCMS Services]
DISCORD[Discord Services]
end
subgraph "Application Layer"
WEB[Web App]
ADMIN[Admin App]
BOT[Discord Bot]
end
CORE --> VALUE_OBJECTS
CORE --> ENTITIES
EVENTS --> CORE
PLAYERS --> CORE
AVAILABILITIES --> CORE
MYSQL --> EVENTS
MYSQL --> PLAYERS
MYSQL --> AVAILABILITIES
PAYLOAD --> EVENTS
PAYLOAD --> PLAYERS
PAYLOAD --> AVAILABILITIES
DISCORD --> EVENTS
DISCORD --> AVAILABILITIES
WEB --> EVENTS
WEB --> PLAYERS
WEB --> AVAILABILITIES
ADMIN --> EVENTS
ADMIN --> PLAYERS
ADMIN --> AVAILABILITIES
BOT --> EVENTS
BOT --> AVAILABILITIESImport Structure
// Feature imports
import { GetNextFewEventsCommandUseCase } from '@/features/events/GetNextFewEventsCommandUseCase'
import { EventCard } from '@/components/events/EventCard'
// Shared imports
import { DateService } from '@/shared/common/src/features/configuration/DateService'
// UI imports
import { Button } from '@/components/ui/Button'
import { useEvents } from '@/hooks/useEvents'⚙️ Configuration Management
Environment Configuration
graph LR
subgraph "Configuration Sources"
ENV[Environment Variables]
CONFIG[Config Files]
SECRETS[Secrets Management]
end
subgraph "Configuration Service"
CONFIG_SERVICE[Configuration Service]
VALIDATION[Validation]
DEFAULTS[Default Values]
end
subgraph "Usage"
APP[Application]
SERVICES[Services]
MIDDLEWARE[Middleware]
end
ENV --> CONFIG_SERVICE
CONFIG --> CONFIG_SERVICE
SECRETS --> CONFIG_SERVICE
CONFIG_SERVICE --> VALIDATION
CONFIG_SERVICE --> DEFAULTS
CONFIG_SERVICE --> APP
CONFIG_SERVICE --> SERVICES
CONFIG_SERVICE --> MIDDLEWAREConfiguration Implementation
export interface ConfigurationRepository {
getDatabaseUrl(): string
getDiscordToken(): string
getEnvironment(): 'development' | 'staging' | 'production'
getFeatureFlag(key: string): boolean
}
export class EnvVarConfigurationRepository implements ConfigurationRepository {
getDatabaseUrl(): string {
const url = process.env.DATABASE_URL
if (!url) {
throw new Error('DATABASE_URL environment variable is required')
}
return url
}
getDiscordToken(): string {
const token = process.env.DISCORD_TOKEN
if (!token) {
throw new Error('DISCORD_TOKEN environment variable is required')
}
return token
}
getEnvironment(): 'development' | 'staging' | 'production' {
return (process.env.NODE_ENV as any) || 'development'
}
getFeatureFlag(key: string): boolean {
return process.env[`FEATURE_${key.toUpperCase()}`] === 'true'
}
}🧪 Testing Strategy
Current Status: No Tests Yet ⚠️
Honest Assessment: As of now, the LIPAIX project does not have any automated tests implemented. This is a known technical debt that needs to be addressed as the project matures.
Why We Don't Have Tests Yet
- 🚧 Rapid Development - The project is still in active development with frequent changes
- 🎯 MVP Focus - Priority has been on getting core functionality working
- 👥 Small Team - Limited resources to implement comprehensive testing
- 🔄 Evolving Architecture - The codebase structure is still being refined
Testing Plan for the Future
graph TD
subgraph "Phase 1: Foundation"
UNIT_TESTS[Unit Tests]
TEST_UTILS[Test Utilities]
MOCK_SYSTEM[Mock System]
end
subgraph "Phase 2: Integration"
API_TESTS[API Tests]
DB_TESTS[Database Tests]
AUTH_TESTS[Authentication Tests]
end
subgraph "Phase 3: End-to-End"
E2E_TESTS[E2E Tests]
USER_FLOWS[User Flow Tests]
CROSS_BROWSER[Cross-browser Tests]
end
UNIT_TESTS --> API_TESTS
API_TESTS --> E2E_TESTS
TEST_UTILS --> UNIT_TESTS
MOCK_SYSTEM --> UNIT_TESTS
DB_TESTS --> API_TESTS
AUTH_TESTS --> API_TESTS
USER_FLOWS --> E2E_TESTS
CROSS_BROWSER --> E2E_TESTSProposed Testing Stack
| Test Type | Tool | Purpose | Priority |
|---|---|---|---|
| Unit Tests | Jest | Business logic validation | High |
| API Tests | Jest + Supertest | Backend API validation | High |
| Component Tests | React Testing Library | Frontend component validation | Medium |
| E2E Tests | Playwright | User journey validation | Low |
| Database Tests | Jest + Test DB | Data layer validation | Medium |
What We'll Test First
- 🧪 Core Business Logic - Use cases, services, and utilities
- 🔌 API Endpoints - REST and GraphQL endpoints
- 🗄️ Database Operations - Repository methods and data validation
- 🎨 UI Components - Critical user interface components
- 🔐 Authentication - Login, permissions, and access control
Testing Guidelines (When We Start)
// Example of how we'll structure tests when we implement them
describe('EventService', () => {
it('should create an event with valid data', async () => {
// Test implementation will go here
})
it('should reject invalid event data', async () => {
// Test implementation will go here
})
})Getting Started with Testing
When the team is ready to implement testing:
- 📚 Learn Testing Tools - Jest, React Testing Library, Playwright
- 🏗️ Set Up Test Infrastructure - Test database, mock services
- 📝 Write First Tests - Start with simple, critical functionality
- 🔄 Integrate with CI/CD - Run tests on every commit
- 📊 Monitor Coverage - Track test coverage metrics
Note: This section will be updated as testing is implemented. For now, manual testing and code review are our primary quality assurance methods.
🚀 Performance Considerations
Performance Architecture
graph TB
subgraph "Frontend Performance"
IMAGE_OPT[Image Optimization]
CODE_SPLIT[Code Splitting]
CACHING[Browser Caching]
end
subgraph "Backend Performance"
DB_OPT[Database Optimization]
API_CACHE[API Caching]
ASYNC[Async Processing]
end
subgraph "Infrastructure"
CDN[CDN]
LOAD_BAL[Load Balancing]
AUTO_SCALE[Auto Scaling]
end
IMAGE_OPT --> CDN
CODE_SPLIT --> LOAD_BAL
CACHING --> CDN
DB_OPT --> AUTO_SCALE
API_CACHE --> LOAD_BAL
ASYNC --> AUTO_SCALECaching Strategy
export class CachedData<T> {
private cache = new Map<string, { data: T; timestamp: number }>()
private ttl: number
constructor(ttlMinutes: number = 5) {
this.ttl = ttlMinutes * 60 * 1000
}
get(key: string): T | null {
const cached = this.cache.get(key)
if (!cached) return null
if (Date.now() - cached.timestamp > this.ttl) {
this.cache.delete(key)
return null
}
return cached.data
}
set(key: string, data: T): void {
this.cache.set(key, { data, timestamp: Date.now() })
}
clear(): void {
this.cache.clear()
}
}🔒 Security Architecture
Security Layers
graph TB
subgraph "Application Security"
AUTH[Authentication]
AUTHORIZATION[Authorization]
VALIDATION[Input Validation]
end
subgraph "Infrastructure Security"
HTTPS[HTTPS/TLS]
FIREWALL[Firewall Rules]
SECRETS[Secrets Management]
end
subgraph "Data Security"
ENCRYPTION[Data Encryption]
BACKUP[Secure Backups]
AUDIT[Audit Logging]
end
AUTH --> HTTPS
AUTHORIZATION --> FIREWALL
VALIDATION --> SECRETS
ENCRYPTION --> BACKUP
BACKUP --> AUDITSecurity Implementation
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Security headers
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
// Content Security Policy
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"
)
return response
}📊 Monitoring & Observability
Monitoring Stack
graph LR
subgraph "Application Monitoring"
SENTRY[Sentry]
LOGS[Structured Logs]
METRICS[Custom Metrics]
end
subgraph "Infrastructure Monitoring"
RAILWAY[Railway Metrics]
HEALTH[Health Checks]
ALERTS[Alerting]
end
subgraph "Performance Monitoring"
CORE_VITALS[Core Web Vitals]
API_METRICS[API Response Times]
ERROR_RATES[Error Rates]
end
SENTRY --> LOGS
LOGS --> METRICS
METRICS --> HEALTH
HEALTH --> ALERTS
CORE_VITALS --> API_METRICS
API_METRICS --> ERROR_RATESHealth Check Implementation
import { NextResponse } from 'next/server'
import { healthCheck } from '@/utils/health'
export async function GET() {
try {
const health = await healthCheck()
if (health.status === 'healthy') {
return NextResponse.json(health, { status: 200 })
} else {
return NextResponse.json(health, { status: 503 })
}
} catch (error) {
return NextResponse.json(
{
status: 'unhealthy',
error: error.message,
timestamp: new Date().toISOString(),
},
{ status: 500 }
)
}
}🚀 Deployment Architecture
Deployment Flow
flowchart TD
A[Code Push] --> B[GitHub Actions]
B --> C[Run Tests]
C --> D{Tests Pass?}
D -->|No| E[Fail Build]
D -->|Yes| F[Build Application]
F --> G[Security Scan]
G --> H{Security OK?}
H -->|No| I[Fail Build]
H -->|Yes| J[Deploy to Railway]
J --> K[Health Check]
K --> L{Health OK?}
L -->|No| M[Rollback]
L -->|Yes| N[Success]
E --> O[Notify Team]
I --> O
M --> O
N --> P[Notify Success]Environment Management
graph LR
subgraph "Environments"
DEV[Development]
STAGING[Staging]
PROD[Production]
end
subgraph "Configuration"
DEV_CONFIG[Dev Config]
STAGING_CONFIG[Staging Config]
PROD_CONFIG[Prod Config]
end
subgraph "Deployment"
DEV_DEPLOY[Local Dev]
STAGING_DEPLOY[Staging Deploy]
PROD_DEPLOY[Production Deploy]
end
DEV --> DEV_CONFIG
STAGING --> STAGING_CONFIG
PROD --> PROD_CONFIG
DEV_CONFIG --> DEV_DEPLOY
STAGING_CONFIG --> STAGING_DEPLOY
PROD_CONFIG --> PROD_DEPLOY🏗️ Architecture Summary
The LIPAIX project follows Clean Architecture principles with practical simplifications. We use a monorepo structure with clear separation of concerns, dependency injection through GlobalRef pattern, and comprehensive testing strategies.
Key architectural features:
- Monorepo Structure - Organized apps, shared code, and documentation
- Clean Architecture - Layered approach with clear dependencies
- Dependency Injection - Simplified GlobalRef pattern for singletons
- Repository Pattern - Abstract data access layer
- Use Case Pattern - Encapsulated business logic
- Comprehensive Testing - Unit, integration, and E2E testing
