Instruction file imported from hktrpg/TG.line.Discord.Roll.Bot (
.cursor/rules/architecture.mdc). Copyright stays with the author.
Architecture & Design Patterns
Solo Developer Principles (Priority)
1. Stability Over Perfection
- Don't refactor working code - If it works and serves users well, leave it alone
- Fix bugs, not architecture - Only change architecture when fixing bugs or adding features
- Incremental improvements - Make small, safe improvements rather than large refactors
- Test before changing - Always verify changes don't break existing functionality
2. Practical Over Perfect
- Simple solutions first - Use the simplest solution that works
- Avoid over-engineering - Don't add complexity "just in case"
- Focus on users - Prioritize features that help users over perfect code
- Maintainability matters - Write code you can understand and fix later
3. Fast Development
- Get it working first - Implement features quickly, optimize later if needed
- Allow shortcuts - Simple, working code is better than complex, perfect code
- Iterate based on feedback - Improve based on actual usage, not theoretical needs
Design Principles (When Needed)
1. Single Responsibility Principle
- Each module/class should have one clear purpose
- Avoid god classes that do too much
- Split large modules only when they become hard to maintain
2. DRY (Don't Repeat Yourself)
- Extract common logic when you see repetition (3+ times)
- Use shared utilities instead of duplicating code
- But don't over-abstract - simple duplication is sometimes fine
3. Keep It Simple
- Prefer simple solutions over complex patterns
- Use existing patterns from the codebase
- Don't introduce new patterns unless necessary
Practical Design Patterns (Use When Helpful)
Manager Pattern (Most Common)
Use for managing resources or state - this is the most useful pattern in this codebase:
class ResourceManager {
constructor() {
this.resources = new Map();
this.initialized = false;
}
async initialize() {
if (this.initialized) return;
// Setup logic
this.initialized = true;
}
async shutdown() {
// Cleanup logic
this.initialized = false;
}
}
Examples in codebase: VIPManager, ModuleManager, PageConfigManager
When to use: Managing state, resources, or configuration that needs initialization/cleanup
Module Pattern (Standard)
Use for modules that need initialization - follow existing patterns:
module.exports = {
async initialize() {
// Setup
},
async shutdown() {
// Cleanup
},
// Public API
publicMethod() {
// Implementation
}
};
When to use: Modules loaded by index.js that need setup/teardown
Observer Pattern (When Needed)
Use EventEmitter for event-driven code - only when events are actually needed:
const EventEmitter = require('events');
class GameEventEmitter extends EventEmitter {
emitGameEvent(eventType, data) {
this.emit(eventType, data);
}
}
Example in codebase: Records class extends EventEmitter
When to use: When you actually need multiple listeners for events (not just for "good design")
When NOT to Use Design Patterns
Avoid Over-Engineering
- ❌ Don't use Factory Pattern - Simple switch/if statements are fine
- ❌ Don't use Strategy Pattern - Functions work just as well
- ❌ Don't use Singleton - Module exports are already singletons in Node.js
- ❌ Don't add patterns "just in case" - Add them when you actually need them
Simple Alternatives
- Use functions instead of Strategy pattern
- Use switch/if instead of Factory pattern
- Use module exports instead of Singleton pattern
- Use simple objects/classes instead of complex hierarchies
Architecture Layers
1. Entry Point (index.js)
- Application initialization
- Module loading
- Error handling
- Graceful shutdown
2. Core Modules (modules/)
- Platform integrations (Discord, Line, etc.)
- Database operations
- Utilities and helpers
- Shared business logic
3. Feature Modules (roll/)
- Game system implementations
- Dice rolling logic
- Character management
- Admin commands
4. Presentation Layer (views/)
- HTML templates
- Client-side JavaScript
- CSS/styles
- User interface
5. Data Layer (modules/db/schema.js)
- Database schemas
- Data models
- Validation rules
Configuration Management
Environment Variables
- Use
process.envfor configuration - Check required vars at module top
- Provide sensible defaults when possible
- Document required environment variables
Constants
- Define constants at module top
- Group related constants in objects
- Use UPPER_SNAKE_CASE naming
- Avoid magic numbers/strings
const CONFIG = {
MAX_RETRIES: 3,
TIMEOUT: 5000,
CACHE_DURATION: 5 * 60 * 1000
};
Error Handling Architecture
Error Handling Strategy
- Use try-catch for async operations
- Provide context in error messages
- Log errors appropriately
- Don't let errors crash the application
- Handle errors at appropriate levels
Error Propagation
async function processData() {
try {
const data = await fetchData();
return process(data);
} catch (error) {
logger.error('Failed to process data', { error, context });
throw error; // Re-throw if caller should handle
}
}
Caching Strategy
When to Cache
- Expensive database queries
- External API calls
- Computed values
- Frequently accessed data
Cache Patterns
class CachedDataManager {
constructor() {
this.cache = new Map();
this.cacheTimeout = 5 * 60 * 1000; // 5 minutes
}
async getData(key) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
return cached.data;
}
const data = await fetchData(key);
this.cache.set(key, { data, timestamp: Date.now() });
return data;
}
}
Database Architecture
Schema Design
- Use Mongoose schemas
- Add proper indexes
- Use consistent field naming
- Follow existing schema patterns
Query Patterns
- Use Mongoose methods over raw queries
- Implement proper error handling
- Use transactions when needed
- Optimize queries with indexes
Connection Management
- Use shared mongoose instance
- Handle connection errors
- Implement reconnection logic
- Monitor connection health
Async/Await Patterns
Sequential Operations
const result1 = await operation1();
const result2 = await operation2(result1);
return result2;
Parallel Operations
const [result1, result2, result3] = await Promise.all([
operation1(),
operation2(),
operation3()
]);
Error Handling in Parallel
const results = await Promise.allSettled([
operation1(),
operation2(),
operation3()
]);
results.forEach((result, index) => {
if (result.status === 'rejected') {
logger.error(`Operation ${index} failed`, result.reason);
}
});
Code Organization
File Structure
- One main class/function per file
- Related utilities in same directory
- Clear separation of concerns
- Logical directory structure
Import Organization
// 1. Node.js built-ins
const fs = require('fs');
const path = require('path');
// 2. External packages
const mongoose = require('mongoose');
const express = require('express');
// 3. Local modules
const schema = require('./db/schema.js');
const utils = require('./utils.js');
Performance Considerations
Optimization Strategies
- Cache expensive operations
- Use database indexes
- Minimize database queries
- Use connection pooling
- Optimize async operations
Memory Management
- Clean up event listeners
- Release resources in shutdown
- Avoid memory leaks
- Monitor memory usage
Scalability (When Needed)
Current Architecture Supports Scaling
- Discord module already uses clustering/sharding
- Database connection pooling is in place
- Stateless design where possible
Optimization Strategy
- Don't optimize prematurely - Only optimize when there's a real performance problem
- Measure first - Profile before optimizing
- Simple optimizations first - Caching, indexes, connection pooling
- Complex optimizations last - Only when simple solutions don't work
Security Considerations
Input Validation
- Validate all user input
- Sanitize data before database operations
- Use parameterized queries
- Check permissions before operations
Sensitive Data
- Don't log sensitive information
- Use environment variables for secrets
- Sanitize logs (see
index.jsLogger) - Handle errors without exposing internals
Documentation (Pragmatic)
Code Documentation
- Document complex logic and non-obvious code
- Use JSDoc for public APIs when helpful
- Don't over-document - Code should be self-explanatory
- Comment why, not what
Architecture Documentation
- Document important decisions and gotchas
- Update README when architecture changes significantly
- Don't document obvious things - Focus on what's not obvious