Clean Architecture Principles
Core architectural principles focused on simplicity, maintainability, and avoiding unnecessary complexity.
Philosophy: Simplicity Over Complexity
If a feature is getting complicated, approach the user about it.
Warn when:
- Engineering becomes complex or complicated
- Multiple approaches exist (suggest simpler one)
- Potential issues are detected
Format:
WARNING: This feature is getting complicated. Here's a simpler approach...
Core Principles
1. DRY (Don't Repeat Yourself)
Code duplication means failure.
Bad - Duplication:
// Creating user
async function createUser(data: UserInput) {
const user = {
id: generateId(),
...data,
createdAt: new Date(),
updatedAt: new Date()
};
await db.users.insert(user);
await sendEmail(user.email, 'Welcome!');
await logEvent('user_created', user.id);
return user;
}
// Creating admin - DUPLICATED LOGIC
async function createAdmin(data: AdminInput) {
const admin = {
id: generateId(),
...data,
createdAt: new Date(),
updatedAt: new Date(),
role: 'admin'
};
await db.users.insert(admin);
await sendEmail(admin.email, 'Welcome Admin!');
await logEvent('admin_created', admin.id);
return admin;
}
Good - DRY:
// Shared logic extracted
async function createUserAccount(
data: UserInput,
role: 'user' | 'admin' = 'user'
) {
const account = {
id: generateId(),
...data,
role,
createdAt: new Date(),
updatedAt: new Date()
};
await db.users.insert(account);
await sendEmail(account.email, getWelcomeMessage(role));
await logEvent(`${role}_created`, account.id);
return account;
}
// Simple wrappers
const createUser = (data: UserInput) => createUserAccount(data, 'user');
const createAdmin = (data: AdminInput) => createUserAccount(data, 'admin');
2. YAGNI (You Ain't Gonna Need It)
Speculation gets rejected.
Bad - Building features "just in case":
interface UserService {
// Currently needed
getUser(id: string): Promise<User>;
// Speculative - might need someday
getAllUsers(): Promise<User[]>;
searchUsers(query: string): Promise<User[]>;
batchUpdateUsers(ids: string[], updates: Partial<User>): Promise<void>;
exportUsersToCSV(): Promise<string>;
importUsersFromCSV(csv: string): Promise<void>;
archiveOldUsers(days: number): Promise<number>;
}
Good - Only what's needed now:
interface UserService {
// Only what's actually needed right now
getUser(id: string): Promise<User>;
}
// Add methods ONLY when requirements emerge
Question everything:
- "Is this really needed?"
- "Will we use this now?"
- "Can we add it later if needed?"
3. KISS (Keep It Simple, Stupid)
Complexity means rewrite.
Bad - Over-engineered:
// Complex factory pattern for simple task
interface INotificationStrategy {
send(message: string): Promise<void>;
}
class EmailNotificationStrategy implements INotificationStrategy {
async send(message: string): Promise<void> {
await emailService.send(message);
}
}
class SMSNotificationStrategy implements INotificationStrategy {
async send(message: string): Promise<void> {
await smsService.send(message);
}
}
class NotificationContext {
constructor(private strategy: INotificationStrategy) {}
async notify(message: string): Promise<void> {
await this.strategy.send(message);
}
}
class NotificationFactory {
static create(type: 'email' | 'sms'): NotificationContext {
const strategy = type === 'email'
? new EmailNotificationStrategy()
: new SMSNotificationStrategy();
return new NotificationContext(strategy);
}
}
// Usage - overly complex
const notifier = NotificationFactory.create('email');
await notifier.notify('Hello');
Good - Simple:
// Simple function - does the same thing
async function sendNotification(message: string, type: 'email' | 'sms') {
if (type === 'email') {
await emailService.send(message);
} else {
await smsService.send(message);
}
}
// Usage - clear and simple
await sendNotification('Hello', 'email');
4. Minimalism
Bloat means delete.
Bad - Bloated with unused features:
class User {
// Actually used
id: string;
name: string;
email: string;
// Unused - delete these
nickname?: string;
favoriteColor?: string;
zodiacSign?: string;
lucky Numbers?: number[];
bio?: string;
website?: string;
socialLinks?: Record<string, string>;
}
Good - Only what's needed:
class User {
id: string;
name: string;
email: string;
}
// Add fields only when actually needed
5. Law of Demeter (Principle of Least Knowledge)
Coupling means refactor.
Bad - Too much coupling:
// Reaching through multiple objects
function displayUserCity(user: User) {
const city = user.profile.address.location.city.name;
console.log(city);
}
// If any intermediate object changes, this breaks
Good - Tell, don't ask:
// User provides what's needed
class User {
getUserCity(): string {
return this.profile.address.getCity();
}
}
function displayUserCity(user: User) {
const city = user.getUserCity();
console.log(city);
}
// Or even simpler
function displayUserCity(city: string) {
console.log(city);
}
6. SoC (Separation of Concerns)
Mixed concerns means split.
Bad - Mixed concerns:
// One function doing everything
async function handleUserRegistration(formData: FormData) {
// Validation
if (!formData.email.includes('@')) throw new Error('Invalid email');
if (formData.password.length < 8) throw new Error('Password too short');
// Business logic
const hashedPassword = await bcrypt.hash(formData.password, 10);
const user = {
id: generateId(),
email: formData.email,
password: hashedPassword,
createdAt: new Date()
};
// Data access
await db.users.insert(user);
// Email sending
await sendEmail(user.email, 'Welcome!');
// Analytics
await analytics.track('user_registered', { userId: user.id });
// UI update
showNotification('Registration successful!');
return user;
}
Good - Separated concerns:
// Validation layer
function validateRegistration(data: FormData): void {
if (!data.email.includes('@')) throw new Error('Invalid email');
if (data.password.length < 8) throw new Error('Password too short');
}
// Business logic layer
async function createUser(data: FormData): Promise<User> {
const hashedPassword = await bcrypt.hash(data.password, 10);
return {
id: generateId(),
email: data.email,
password: hashedPassword,
createdAt: new Date()
};
}
// Data access layer
async function saveUser(user: User): Promise<void> {
await db.users.insert(user);
}
// Notification layer
async function notifyNewUser(user: User): Promise<void> {
await sendEmail(user.email, 'Welcome!');
await analytics.track('user_registered', { userId: user.id });
}
// Orchestration - clean and clear
async function handleUserRegistration(formData: FormData) {
validateRegistration(formData);
const user = await createUser(formData);
await saveUser(user);
await notifyNewUser(user);
showNotification('Registration successful!');
return user;
}
Filipino Wisdom Integration
"Wag mong isama kung hindi mo kailangan"
Apply to every line of code.
Questions to ask:
- Do we really need this line?
- Is this variable actually used?
- Can we remove this abstraction?
- Is this dependency necessary?
Examples:
Bad:
// Including things not needed
import { useEffect, useState, useCallback, useMemo, useRef } from 'react';
function SimpleComponent() {
const [count, setCount] = useState(0);
// useEffect, useCallback, useMemo, useRef not used - remove them
return <div>{count}</div>;
}
Good:
// Only what's needed
import { useState } from 'react';
function SimpleComponent() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
Clarity Beats Cleverness
Every single time.
Bad - Clever but unclear:
// "Clever" one-liner
const result = users.reduce((a, u) => ({...a, [u.id]: u.posts.filter(p => p.likes > 10).map(p => p.title)}), {});
Good - Clear and understandable:
// Clear multi-step process
const result: Record<string, string[]> = {};
for (const user of users) {
const popularPosts = user.posts.filter(post => post.likes > 10);
const titles = popularPosts.map(post => post.title);
result[user.id] = titles;
}
Warning System
Always warn when complexity creeps in:
WARNING: This authentication flow is getting complicated with 5+ steps.
Simpler approach:
1. Use existing authentication library (e.g., NextAuth.js)
2. Reduces custom code by 80%
3. Battle-tested security
4. Maintained by community
Current approach requires:
- Custom JWT handling
- Manual token refresh
- Session management
- Security auditing
Recommendation: Use established solution instead.
Questioning Requirements
Never just accept requirements blindly.
Example conversation:
User: "Add a feature where users can export their data to XML, JSON, CSV, and YAML formats"
Response:
"Let me question this requirement:
1. Which format will users actually use?
2. Can we start with just JSON (most common)?
3. Add other formats only if users request them?
Starting simple:
- Implement JSON export first
- Monitor usage
- Add other formats if needed
This approach:
✅ Delivers value faster
✅ Avoids building unused features
✅ Easier to maintain
✅ Can always add more later
Does JSON-only work for the initial release?"
Complexity Detection Checklist
Warn if you detect:
- [ ] More than 3 levels of nesting
- [ ] Functions longer than 50 lines
- [ ] More than 5 function parameters
- [ ] Deeply nested object access (user.a.b.c.d.e)
- [ ] Multiple concerns in one function
- [ ] Duplicate code in 3+ places
- [ ] Speculative features not in requirements
- [ ] Over-engineered patterns for simple tasks
- [ ] Unnecessary abstractions or layers
- [ ] Complex logic that could be simplified
Refactoring Triggers
Refactor when:
-
Code Duplication (DRY violation)
- Same logic in multiple places
- Copy-paste code
-
High Coupling (Law of Demeter violation)
- Reaching through multiple objects
- Changes ripple across many files
-
Mixed Concerns (SoC violation)
- One function doing multiple things
- Hard to understand or test
-
Unnecessary Complexity (KISS violation)
- Over-engineered solutions
- Complex patterns for simple problems
-
Speculative Code (YAGNI violation)
- Unused features or functions
- "Just in case" code
Architecture Decision Template
Before building a feature, ask:
## Feature: [Name]
### Is this really needed?
- [ ] Yes, it's in current requirements
- [ ] No, it's speculative
### Can we make it simpler?
Current approach: [Describe]
Simpler approach: [Describe]
### Does it follow principles?
- [ ] DRY - No code duplication
- [ ] YAGNI - Not speculative
- [ ] KISS - Simple, not complex
- [ ] SoC - Concerns separated
- [ ] Minimalism - Only what's needed
### Warning flags:
- Complexity level: [Low/Medium/High]
- Dependencies added: [Number]
- Lines of code: [Estimate]
### Recommendation:
[Approve / Simplify / Reject with reasons]
Summary: Keep It Simple
Golden Rules:
-
Question Everything
- "Is this really needed?"
- "Can we make it simpler?"
-
Avoid Complexity
- Simple solutions over clever ones
- Warn when complexity creeps in
-
Follow Principles
- DRY - Don't repeat yourself
- YAGNI - You ain't gonna need it
- KISS - Keep it simple
- Minimalism - Only what's needed
- Law of Demeter - Minimal coupling
- SoC - Separate concerns
-
Filipino Wisdom
- "Wag mong isama kung hindi mo kailangan"
- Apply to every line
-
Clarity Always Wins
- Clear code beats clever code
- Every single time
Remember: The best architecture is the simplest one that solves the actual problem. Don't build what you might need—build what you need now.
Scan to join WeChat group