| name | enforce-modularity |
| description | Apply Single Responsibility Principle when writing code. Keep modules focused, cohesive, and loosely coupled. Guide splitting classes with too many methods. Ensure code is reusable and testable. |
Modularity Standards
CRITICAL: Write modular, focused, maintainable code. Each module should do ONE thing well.
Why This Exists
Monolithic code:
- Hard to understand
- Hard to test
- Hard to maintain
- Hard to reuse
- Easy to break
Modular code is easier to understand, test, maintain, reuse, and evolve.
When to Apply
Apply modularity when:
- Creating new files/classes/functions
- Refactoring existing code
- Reviewing code quality
- Planning architecture
How It Works
1. Single Responsibility Principle (SRP)
Each file, class, and function should do ONE thing well.
File Level
- Each file represents ONE concept/responsibility
- File name clearly indicates single purpose
- If you can't name it without "and" or "or", split it
Class Level
- Each class has ONE reason to change
- Maximum 7-10 public methods per class
- Private methods don't count toward limit
Function Level
- Each function does ONE task
- Maximum 20-30 lines per function
- Maximum 3-4 parameters per function
- If needs many params, group into object
2. High Cohesion
Functions in a module work on related data.
Good cohesion:
- ✅ All code in file serves module's single purpose
- ✅ Functions work on same data structures
- ✅ Changes to one part don't cascade
Bad cohesion:
- ❌ Unrelated functions grouped together
- ❌ File does multiple unrelated things
- ❌ Changes require editing multiple sections
3. Low Coupling
Modules depend on interfaces, not implementations.
Good coupling:
- ✅ Depend on abstractions (interfaces)
- ✅ Easy to swap implementations
- ✅ Changes don't cascade to other modules
Bad coupling:
- ❌ Direct dependencies on concrete classes
- ❌ Hard to test in isolation
- ❌ Changes break other modules
Examples
Good: Single Responsibility Per Service
// ✅ UserService.ts - User management only
class UserService {
createUser() {}
updateUser() {}
deleteUser() {}
findUserById() {}
}
// ✅ EmailService.ts - Email only
class EmailService {
sendWelcomeEmail() {}
sendPasswordResetEmail() {}
sendNotification() {}
}
// ✅ TokenService.ts - Tokens only
class TokenService {
generateToken() {}
verifyToken() {}
refreshToken() {}
}
Bad: Multiple Responsibilities
// ❌ UserService.ts - Too many responsibilities!
class UserService {
// User management
createUser() {}
updateUser() {}
// Email (should be EmailService)
sendEmail() {}
// Logging (should be Logger)
logActivity() {}
// Validation (should be Validator)
validatePassword() {}
// Auth (should be TokenService)
generateToken() {}
}
Good: High Cohesion, Low Coupling
// ✅ Interface for abstraction
interface IEmailProvider {
send(to: string, subject: string, body: string): Promise<void>;
}
// ✅ Service depends on interface, not concrete implementation
class EmailService {
constructor(private provider: IEmailProvider) {}
async sendWelcomeEmail(user: User) {
await this.provider.send(
user.email,
'Welcome!',
this.buildWelcomeBody(user)
);
}
// All email logic together (high cohesion)
private buildWelcomeBody(user: User): string {
return `Welcome ${user.name}!`;
}
}
// ✅ Provider can be swapped without changing EmailService (low coupling)
class SendGridProvider implements IEmailProvider {
async send(to: string, subject: string, body: string) {
// SendGrid implementation
}
}
class MailgunProvider implements IEmailProvider {
async send(to: string, subject: string, body: string) {
// Mailgun implementation
}
}
Good: Testable in Isolation
// ✅ Pure, testable function
class PasswordValidator {
validateStrength(password: string): boolean {
return (
password.length >= 8 &&
/[A-Z]/.test(password) &&
/[0-9]/.test(password)
);
}
}
// Test is simple
test('validates strong password', () => {
const validator = new PasswordValidator();
expect(validator.validateStrength('Password123')).toBe(true);
});
Bad: Hard to Test
// ❌ Tightly coupled to framework, database, everything
class UserController {
createUser(req: Request, res: Response) {
// Validation mixed in
if (!req.body.email) {
return res.status(400).json({ error: 'Email required' });
}
// Database access mixed in
const user = await db.users.create(req.body);
// Email sending mixed in
await sendgrid.send({
to: user.email,
subject: 'Welcome'
});
// Response handling mixed in
res.json(user);
}
}
// Test requires mocking: HTTP framework, database, sendgrid, etc.
Module Organization
Feature-Based Structure
src/features/auth/
├── controllers/ # HTTP handlers (max 300 lines)
├── services/ # Business logic (max 300 lines)
├── repositories/ # Data access (max 300 lines)
├── models/ # Domain models (max 200 lines)
├── validators/ # Validation (max 150 lines)
├── types/ # TypeScript types (max 100 lines)
└── utils/ # Feature utilities (max 200 lines)
Shared vs Core
shared/ # Used by 2+ features
├── services/
├── utils/
├── types/
└── constants/
core/ # Infrastructure (rarely changes)
├── database/
├── config/
├── errors/
└── interfaces/
Code Reusability
Before writing ANY code:
Search for existing functionality
- Check
shared/utils/ - Look in related features
- Search for similar patterns
- Check
Extract common logic
- Duplicated in 2+ places? Extract it
- Function > 20 lines and reusable? Extract it
- Pattern could benefit others? Extract it
Choose location based on usage:
Used by ONE feature: feature/utils/ Used by 2+ features: shared/utils/ Used by ALL features: core/utils/
Example: Extracting Common Logic
// ❌ BEFORE: Duplicated in multiple files
// UserController.ts
async createUser(req, res) {
if (!req.body.email || !req.body.email.includes('@')) {
return res.status(400).json({ error: 'Invalid email' });
}
// ...
}
// PostController.ts
async createPost(req, res) {
if (!req.body.email || !req.body.email.includes('@')) {
return res.status(400).json({ error: 'Invalid email' });
}
// ...
}
// ✅ AFTER: Extracted to shared utility
// shared/utils/validation.ts
export function validateEmail(email: string): boolean {
return email && email.includes('@') && email.length > 3;
}
// Now used everywhere
import { validateEmail } from '@/shared/utils/validation';
if (!validateEmail(req.body.email)) {
return res.status(400).json({ error: 'Invalid email' });
}
Splitting Large Modules
When to Split
Split when:
- File approaching 400 lines
- Class has > 10 public methods
- Function > 30 lines
- Multiple responsibilities detected
- Hard to test in isolation
How to Split
Strategy 1: Extract Utilities
// ❌ BEFORE: UserService.ts (550 lines)
class UserService {
validateEmail() { /* 50 lines */ }
hashPassword() { /* 50 lines */ }
generateToken() { /* 50 lines */ }
createUser() { /* 400 lines */ }
}
// ✅ AFTER: Split utilities
// utils/emailValidation.ts
export function validateEmail() {}
// utils/passwordUtils.ts
export function hashPassword() {}
// utils/tokenGenerator.ts
export function generateToken() {}
// UserService.ts (400 lines)
import { validateEmail } from '../utils/emailValidation';
import { hashPassword } from '../utils/passwordUtils';
class UserService {
createUser() { /* uses imported utilities */ }
}
Strategy 2: Extract Sub-Services
// ❌ BEFORE: UserService.ts (600 lines)
class UserService {
// User CRUD (200 lines)
// Password management (200 lines)
// Profile management (200 lines)
}
// ✅ AFTER: Split by domain
// UserService.ts (200 lines)
class UserService {
createUser() {}
updateUser() {}
deleteUser() {}
}
// UserPasswordService.ts (200 lines)
class UserPasswordService {
changePassword() {}
resetPassword() {}
}
// UserProfileService.ts (200 lines)
class UserProfileService {
updateProfile() {}
uploadAvatar() {}
}
Enforcement
The code-quality-auditor will reject:
- ❌ Files > 500 lines
- ❌ Classes with > 10 public methods
- ❌ Functions > 30 lines
- ❌ Functions with > 4 parameters
- ❌ Multiple responsibilities in one file
- ❌ Duplicated code (DRY violations)
- ❌ Tight coupling to implementations
Quick Checklist
- Each file has single responsibility
- File name clearly describes purpose
- Classes have < 10 public methods
- Functions are < 30 lines
- Functions have < 4 parameters
- No code duplication
- Modules are loosely coupled (interfaces)
- Code is testable in isolation
- Reusable logic extracted to utilities