Episode 9 — System Design / 9.2 — Design Principles
9.2.b — DRY and Other Principles
In one sentence: Beyond SOLID, a handful of time-tested principles — DRY, KISS, YAGNI, Composition over Inheritance, Law of Demeter, and Separation of Concerns — give you a practical toolkit for deciding what to build, how to structure it, and when to stop adding complexity.
Navigation: ← SOLID Principles · Next → Introduction to Design Patterns
Table of Contents
- DRY — Don't Repeat Yourself
- KISS — Keep It Simple, Stupid
- YAGNI — You Ain't Gonna Need It
- Composition over Inheritance
- Law of Demeter (Principle of Least Knowledge)
- Separation of Concerns (SoC)
- Over-Engineering vs Under-Engineering
- When Each Principle Applies — Decision Matrix
- Key Takeaways
- Explain-It Challenge
1. DRY — Don't Repeat Yourself
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." — Andy Hunt & Dave Thomas, The Pragmatic Programmer
DRY is not about eliminating duplicate lines of code. It's about eliminating duplicate knowledge — the same business rule, algorithm, or decision expressed in multiple places.
What DRY Really Means
DRY is about KNOWLEDGE, not about CODE.
Two identical lines of code that represent DIFFERENT concepts
are NOT a DRY violation.
Two different-looking blocks that encode the SAME business rule
ARE a DRY violation.
DRY Violation: Duplicated Business Logic
// BEFORE: The same validation rule is expressed in THREE places
// In the signup route
app.post('/signup', (req, res) => {
if (req.body.password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
if (!/[A-Z]/.test(req.body.password)) {
return res.status(400).json({ error: 'Password must contain uppercase' });
}
// ...
});
// In the password reset route
app.post('/reset-password', (req, res) => {
if (req.body.newPassword.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
if (!/[A-Z]/.test(req.body.newPassword)) {
return res.status(400).json({ error: 'Password must contain uppercase' });
}
// ...
});
// In the admin panel
app.post('/admin/create-user', (req, res) => {
if (req.body.password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
if (!/[A-Z]/.test(req.body.password)) {
return res.status(400).json({ error: 'Password must contain uppercase' });
}
// ...
});
// Problem: Security team says "add a special character requirement."
// You update signup... forget reset-password... admin panel stays stale.
// Now your system has INCONSISTENT password rules.
// AFTER: One authoritative source of truth for password validation
interface ValidationResult {
valid: boolean;
errors: string[];
}
function validatePassword(password: string): ValidationResult {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain an uppercase letter');
}
if (!/[!@#$%^&*]/.test(password)) {
errors.push('Password must contain a special character');
}
return { valid: errors.length === 0, errors };
}
// All three routes use the same function
app.post('/signup', (req, res) => {
const result = validatePassword(req.body.password);
if (!result.valid) return res.status(400).json({ errors: result.errors });
// ...
});
app.post('/reset-password', (req, res) => {
const result = validatePassword(req.body.newPassword);
if (!result.valid) return res.status(400).json({ errors: result.errors });
// ...
});
// Change the rule in ONE place, it's consistent EVERYWHERE.
When Identical Code Is NOT a DRY Violation
// These two functions look similar but represent DIFFERENT business concepts
function calculateEmployeeTax(salary: number): number {
return salary * 0.30; // Employee income tax rate
}
function calculateSalesTax(amount: number): number {
return amount * 0.30; // Sales tax rate (coincidentally the same)
}
// DO NOT merge these into one function!
// They look the same today but change for DIFFERENT reasons:
// - Employee tax changes when tax law changes
// - Sales tax changes when the business enters a new state/country
// Merging them creates COUPLING between unrelated concepts.
DRY for Configuration
// BEFORE: Magic numbers scattered everywhere
app.post('/api/upload', (req, res) => {
if (req.file.size > 5242880) { /* ... */ } // What is 5242880?
});
app.post('/api/avatar', (req, res) => {
if (req.file.size > 5242880) { /* ... */ } // Same magic number
});
// AFTER: Single source of truth
const LIMITS = {
MAX_FILE_SIZE: 5 * 1024 * 1024, // 5 MB — self-documenting
MAX_AVATAR_SIZE: 2 * 1024 * 1024, // 2 MB
MAX_FILES_PER_UPLOAD: 10,
} as const;
app.post('/api/upload', (req, res) => {
if (req.file.size > LIMITS.MAX_FILE_SIZE) { /* ... */ }
});
The WET Spectrum (Write Everything Twice)
Some teams follow the "Rule of Three": don't abstract until you see the pattern three times. This avoids premature abstraction.
1st occurrence: Just write it.
2nd occurrence: Note the duplication, but leave it.
3rd occurrence: Now extract a shared abstraction.
2. KISS — Keep It Simple, Stupid
"Simplicity is the ultimate sophistication." — Leonardo da Vinci
The simplest solution that works is usually the best. Complexity is a cost, not a feature.
KISS Violation: Over-Clever Code
// BEFORE: "Look how clever I am!"
const getActiveAdminEmails = (users: User[]) =>
users.reduce((acc, u) =>
u.role === 'admin' && u.active
? [...acc, u.email]
: acc,
[] as string[]
);
// AFTER: "Look how clear I am."
function getActiveAdminEmails(users: User[]): string[] {
return users
.filter(user => user.role === 'admin' && user.active)
.map(user => user.email);
}
// Same result, but anyone can read it in 2 seconds.
KISS Violation: Premature Architecture
// BEFORE: "What if we need to support multiple queue providers?"
// (You have ONE queue. You use Redis. You will use Redis for the next 3 years.)
interface QueueProvider {
enqueue(job: Job): Promise<void>;
dequeue(): Promise<Job | null>;
acknowledge(jobId: string): Promise<void>;
}
interface QueueSerializer {
serialize(job: Job): string;
deserialize(data: string): Job;
}
interface QueueRetryStrategy {
shouldRetry(job: Job, attempts: number): boolean;
getDelay(attempts: number): number;
}
class QueueManager {
constructor(
private provider: QueueProvider,
private serializer: QueueSerializer,
private retryStrategy: QueueRetryStrategy,
private logger: Logger,
private metrics: MetricsCollector
) {}
// 200 lines of orchestration...
}
// AFTER: Just use Bull/BullMQ. Done.
import { Queue, Worker } from 'bullmq';
const emailQueue = new Queue('emails', { connection: redisConfig });
emailQueue.add('welcome', { userId: '123', template: 'welcome' });
new Worker('emails', async (job) => {
await sendEmail(job.data);
}, { connection: redisConfig });
KISS Decision Framework
| Situation | Simple Approach | Over-Engineered Approach |
|---|---|---|
| Need a config file | .env + dotenv | Custom YAML parser with schema validation and hot reloading |
| Need to call an API | fetch() or axios | Custom HTTP client with retry, circuit breaker, and request signing |
| Need a state machine | Object with status field + switch | Full state machine library with transition guards and plugins |
| Need to schedule a task | setInterval or cron job | Custom job scheduler with priority queues and distributed locking |
Rule of thumb: Use libraries and existing tools before building your own. Build your own only when existing solutions genuinely don't fit.
3. YAGNI — You Ain't Gonna Need It
"Always implement things when you actually need them, never when you just foresee that you need them." — Ron Jeffries
YAGNI fights speculative complexity — code written "just in case" that never gets used.
YAGNI Violation: Building for a Future That Never Comes
// BEFORE: "What if we need multi-tenancy someday?"
interface TenantConfig {
tenantId: string;
databaseUrl: string;
features: string[];
theme: ThemeConfig;
customDomain?: string;
}
class TenantAwareRepository<T> {
constructor(
private tenantResolver: TenantResolver,
private connectionPool: TenantConnectionPool,
private cacheStrategy: TenantCacheStrategy
) {}
async find(id: string): Promise<T> {
const tenant = this.tenantResolver.getCurrentTenant();
const connection = await this.connectionPool.getConnection(tenant.tenantId);
const cacheKey = `${tenant.tenantId}:${id}`;
// ... 50 more lines of tenant-aware logic
}
}
// Reality: You have ONE customer. You will have ONE customer for the next year.
// You just spent 3 weeks building multi-tenancy nobody asked for.
// AFTER: Build what you need NOW
class UserRepository {
async find(id: string): Promise<User | null> {
const result = await db.query('SELECT * FROM users WHERE id = $1', [id]);
return result.rows[0] || null;
}
}
// When you actually get a second tenant, THEN design multi-tenancy —
// and you'll design it better because you'll understand the real requirements.
The Cost of YAGNI Violations
Speculative feature written today:
+ 2 days to build
+ 1 day to test
+ Ongoing maintenance cost
+ Adds complexity that slows down REAL features
+ 70% chance it's never used
+ If it IS used, requirements have changed and it needs rewriting anyway
Feature built when actually needed:
+ Built with real requirements (not guesses)
+ Designed with actual use cases
+ Minimal wasted effort
YAGNI Does NOT Mean
- "Never plan ahead" — Architecture decisions (database choice, API style) DO need forethought.
- "Don't write clean code" — YAGNI is about features, not quality.
- "Don't use design patterns" — Use patterns when you have a real problem, not a hypothetical one.
YAGNI applies to: Features, abstractions, support for hypothetical scenarios
YAGNI does NOT apply to: Tests, error handling, security, documentation
4. Composition over Inheritance
"Favor object composition over class inheritance." — Gang of Four, Design Patterns
Inheritance creates rigid hierarchies. Composition creates flexible, interchangeable parts.
The Inheritance Problem: The Diamond of Death
// BEFORE: Inheritance hierarchy that becomes a trap
class Animal {
eat() { console.log('Eating'); }
sleep() { console.log('Sleeping'); }
}
class FlyingAnimal extends Animal {
fly() { console.log('Flying'); }
}
class SwimmingAnimal extends Animal {
swim() { console.log('Swimming'); }
}
// Duck can fly AND swim. Which do we extend?
// JavaScript doesn't have multiple inheritance!
class Duck extends FlyingAnimal {
// We get fly() from FlyingAnimal, but we also need swim()!
swim() { console.log('Swimming'); } // Manually duplicate — violates DRY
}
// Penguin swims but doesn't fly...
class Penguin extends SwimmingAnimal {
// OK, but what if penguins need to waddle like a LandAnimal?
}
// As you add more behaviors (running, climbing, burrowing),
// the hierarchy becomes impossible to manage.
The Fix: Composition with Mixins/Interfaces
// AFTER: Compose behaviors instead of inheriting them
// Define behaviors as independent, composable units
interface Swimmer {
swim(): void;
}
interface Flyer {
fly(): void;
}
interface Runner {
run(): void;
}
// Behavior implementations (reusable)
const swimmable = {
swim() { console.log(`${this.name} is swimming`); }
};
const flyable = {
fly() { console.log(`${this.name} is flying`); }
};
const runnable = {
run() { console.log(`${this.name} is running`); }
};
// Compose behaviors freely
class Duck implements Swimmer, Flyer, Runner {
name = 'Duck';
swim = swimmable.swim.bind(this);
fly = flyable.fly.bind(this);
run = runnable.run.bind(this);
}
class Penguin implements Swimmer, Runner {
name = 'Penguin';
swim = swimmable.swim.bind(this);
run = runnable.run.bind(this);
// No fly — penguins can't fly, and we don't pretend they can
}
Composition in Practice: TypeScript Class Composition
// Real-world example: Building user notification preferences
// INHERITANCE APPROACH (rigid)
class BaseNotifier {
notify(message: string) { /* ... */ }
}
class EmailNotifier extends BaseNotifier { /* ... */ }
class EmailAndSMSNotifier extends EmailNotifier { /* adds SMS somehow */ }
class EmailAndSMSAndPushNotifier extends EmailAndSMSNotifier { /* adds push somehow */ }
// Explosion of classes for every combination!
// COMPOSITION APPROACH (flexible)
interface NotificationChannel {
send(userId: string, message: string): Promise<void>;
}
class EmailChannel implements NotificationChannel {
async send(userId: string, message: string): Promise<void> {
console.log(`Email to ${userId}: ${message}`);
}
}
class SMSChannel implements NotificationChannel {
async send(userId: string, message: string): Promise<void> {
console.log(`SMS to ${userId}: ${message}`);
}
}
class PushChannel implements NotificationChannel {
async send(userId: string, message: string): Promise<void> {
console.log(`Push to ${userId}: ${message}`);
}
}
class SlackChannel implements NotificationChannel {
async send(userId: string, message: string): Promise<void> {
console.log(`Slack to ${userId}: ${message}`);
}
}
// Compose any combination dynamically
class NotificationService {
private channels: NotificationChannel[] = [];
addChannel(channel: NotificationChannel): void {
this.channels.push(channel);
}
async notifyAll(userId: string, message: string): Promise<void> {
await Promise.all(
this.channels.map(channel => channel.send(userId, message))
);
}
}
// Usage — compose at runtime based on user preferences
const notifier = new NotificationService();
notifier.addChannel(new EmailChannel());
notifier.addChannel(new PushChannel());
// User changes preferences? Add/remove channels. No class hierarchy changes.
if (userPreferences.wantsSMS) {
notifier.addChannel(new SMSChannel());
}
When Inheritance IS Appropriate
Inheritance is not evil — it's just overused. Use it when:
| Use Inheritance When | Use Composition When |
|---|---|
| There's a genuine is-a relationship | There's a has-a or uses-a relationship |
| Subtypes truly share behavior AND identity | Objects share some behaviors but not others |
| The hierarchy is shallow (2-3 levels max) | You need to combine behaviors flexibly |
| You're extending a framework class (React Component) | You're building your own domain model |
5. Law of Demeter (Principle of Least Knowledge)
"Only talk to your immediate friends. Don't talk to strangers."
A method should only call methods on:
- Its own object (
this) - Objects passed as parameters
- Objects it creates
- Its direct component objects (properties)
The Violation: Train Wrecks
// BEFORE: Chaining through objects you don't own — "train wreck" code
// Getting a user's city requires knowing the internal structure of 4 objects
const city = order.getCustomer().getAddress().getCity().getName();
// What happens when:
// - Customer changes how addresses are stored? Code breaks.
// - Address becomes optional? NullPointerException.
// - City is renamed to Municipality? Every caller must change.
// This function knows WAY too much about the internal structure
function getShippingLabel(order: Order): string {
const customer = order.getCustomer();
const address = customer.getAddress();
const city = address.getCity();
const state = address.getState();
const zip = address.getZipCode();
return `${customer.getName()}\n${address.getStreet()}\n${city}, ${state} ${zip}`;
}
// AFTER: Each object exposes what its callers need
class Order {
getShippingLabel(): string {
return this.customer.getFormattedAddress();
}
getDeliveryCity(): string {
return this.customer.getCity();
}
}
class Customer {
getFormattedAddress(): string {
return this.address.format();
}
getCity(): string {
return this.address.getCity();
}
}
class Address {
format(): string {
return `${this.street}\n${this.city}, ${this.state} ${this.zip}`;
}
getCity(): string {
return this.city;
}
}
// Caller only talks to its immediate friend (order)
const label = order.getShippingLabel();
Law of Demeter in Express Middleware
// BAD: Reaching deep into req object internals
function getUsername(req: Request): string {
return req.session.passport.user.profile.displayName;
// If ANY of these nested objects changes, this breaks
}
// GOOD: Provide a clean accessor
function getUsername(req: Request): string {
return req.user?.displayName ?? 'Anonymous';
// Middleware sets req.user — we don't care how authentication works internally
}
When to Bend the Law of Demeter
The Law of Demeter is a guideline, not a law. It's acceptable to chain through:
- Data structures —
user.address.cityon a plain data object is fine - Fluent APIs —
query.where('age', '>', 18).orderBy('name').limit(10)(each method returns the same builder) - Functional chains —
array.filter(...).map(...).reduce(...)(each returns a new value)
The law targets behavioral objects, not data containers or builder patterns.
6. Separation of Concerns (SoC)
"Divide your program such that each section addresses a separate concern." — Edsger W. Dijkstra
A concern is a distinct area of functionality. SoC says that code handling different concerns should be in different modules.
SoC in a Full-Stack Application
┌─────────────────────────────────────────────┐
│ FRONTEND │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ View │ │ State │ │ Routing │ │
│ │ (React) │ │ (Redux) │ │ (Router) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────┤
│ API LAYER │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Routes │ │ Auth │ │ Validation│ │
│ │ │ │Middleware│ │Middleware │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────┤
│ BUSINESS LOGIC │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ User │ │ Order │ │ Payment │ │
│ │ Service │ │ Service │ │ Service │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────┤
│ DATA ACCESS │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ User │ │ Order │ │ Payment │ │
│ │ Repo │ │ Repo │ │ Repo │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
SoC Violation and Fix
// BEFORE: One function handles HTTP, validation, business logic, and data access
app.post('/api/orders', async (req, res) => {
// CONCERN 1: HTTP parsing
const { items, address, paymentMethod } = req.body;
// CONCERN 2: Validation
if (!items || items.length === 0) {
return res.status(400).json({ error: 'No items' });
}
if (!address) {
return res.status(400).json({ error: 'No address' });
}
// CONCERN 3: Business logic
let total = 0;
for (const item of items) {
const product = await db.query('SELECT price FROM products WHERE id = $1', [item.id]);
total += product.rows[0].price * item.quantity;
}
const tax = total * 0.08;
const shipping = total > 50 ? 0 : 9.99;
// CONCERN 4: Payment processing
const charge = await stripe.charges.create({
amount: Math.round((total + tax + shipping) * 100),
currency: 'usd',
source: paymentMethod,
});
// CONCERN 5: Data persistence
await db.query(
'INSERT INTO orders (items, total, tax, shipping, charge_id) VALUES ($1, $2, $3, $4, $5)',
[JSON.stringify(items), total, tax, shipping, charge.id]
);
// CONCERN 6: Notification
await sendOrderConfirmationEmail(req.user.email, { items, total, tax, shipping });
res.status(201).json({ orderId: charge.id, total: total + tax + shipping });
});
// AFTER: Each concern is separated into its own layer
// Validation (concern: input correctness)
const validateOrder = (req: Request, res: Response, next: NextFunction) => {
const { error } = orderSchema.validate(req.body);
if (error) return res.status(400).json({ error: error.message });
next();
};
// Controller (concern: HTTP interface)
class OrderController {
constructor(private orderService: OrderService) {}
create = async (req: Request, res: Response) => {
try {
const order = await this.orderService.placeOrder(req.body, req.user);
res.status(201).json(order);
} catch (err) {
if (err instanceof PaymentError) {
res.status(402).json({ error: err.message });
} else {
res.status(500).json({ error: 'Order failed' });
}
}
};
}
// Service (concern: business logic)
class OrderService {
constructor(
private orderRepo: OrderRepository,
private productRepo: ProductRepository,
private paymentService: PaymentService,
private notificationService: NotificationService
) {}
async placeOrder(data: CreateOrderDTO, user: User): Promise<OrderResult> {
const total = await this.calculateTotal(data.items);
const tax = total * 0.08;
const shipping = total > 50 ? 0 : 9.99;
const charge = await this.paymentService.charge(
total + tax + shipping,
data.paymentMethod
);
const order = await this.orderRepo.create({
items: data.items,
total, tax, shipping,
chargeId: charge.id,
});
await this.notificationService.sendOrderConfirmation(user.email, order);
return { orderId: order.id, total: total + tax + shipping };
}
private async calculateTotal(items: OrderItem[]): Promise<number> {
let total = 0;
for (const item of items) {
const product = await this.productRepo.findById(item.id);
total += product.price * item.quantity;
}
return total;
}
}
// Repository (concern: data access)
class OrderRepository {
async create(data: CreateOrderData): Promise<Order> {
const result = await db.query(
'INSERT INTO orders (items, total, tax, shipping, charge_id) VALUES ($1, $2, $3, $4, $5) RETURNING *',
[JSON.stringify(data.items), data.total, data.tax, data.shipping, data.chargeId]
);
return result.rows[0];
}
}
// Route — thin wiring
app.post('/api/orders', authenticate, validateOrder, orderController.create);
7. Over-Engineering vs Under-Engineering
Finding the right balance is the hardest skill in software design.
Signs of Over-Engineering
// Over-engineered: A "Hello World" API with enterprise architecture
// AbstractGreetingStrategyFactory.ts
// GreetingStrategyFactoryImpl.ts
// IGreetingService.ts
// GreetingServiceImpl.ts
// GreetingDTO.ts
// GreetingMapper.ts
// GreetingValidator.ts
// GreetingRepository.ts
// GreetingController.ts
// GreetingMiddleware.ts
// GreetingConfig.ts
// All to do this:
app.get('/hello', (req, res) => res.json({ message: 'Hello, World!' }));
Signs of Under-Engineering
// Under-engineered: An e-commerce platform in one file
// app.js — 3000 lines
// - Database queries inline
// - Business logic mixed with HTTP handling
// - No error handling
// - Hardcoded credentials
// - No validation
// - Copy-pasted code everywhere
The Balance Spectrum
Under-Engineered Just Right Over-Engineered
──────────────────────────────────────────────────────────────────
Spaghetti code Clean, readable Enterprise Java
No abstractions Meaningful abstractions Abstraction for its own sake
No patterns Patterns where needed Patterns everywhere
"It works" "It works and is clear" "It works but nobody can follow it"
1 file, 3000 lines Right-sized modules 200 files, 50 lines each
Changes break everything Changes are localized Changes require touching 12 files
Decision Framework
| Factor | Lean Toward Simple | Lean Toward Architecture |
|---|---|---|
| Team size | 1-2 developers | 5+ developers |
| Expected lifespan | Prototype / script / one-off | Production system for years |
| Change frequency | Rarely changes | Changes weekly |
| Domain complexity | Simple CRUD | Complex business rules |
| Criticality | Internal tool | Payment processing, healthcare |
The "Will I Thank Myself?" Test
Before adding abstraction, ask:
- "Am I solving a problem I HAVE, or a problem I MIGHT have?" If might, wait (YAGNI).
- "Can a new team member understand this in 10 minutes?" If not, simplify (KISS).
- "If I delete this abstraction, does the code still work?" If yes, it's unnecessary.
- "Am I adding this because the code NEEDS it, or because I SAW it in a blog post?" Be honest.
8. When Each Principle Applies — Decision Matrix
| Principle | Apply When | Don't Over-Apply When |
|---|---|---|
| DRY | Same business rule in 3+ places | Two code blocks look similar but represent different concepts |
| KISS | Always — default to simplicity | Never stop applying; complexity should be justified |
| YAGNI | You're tempted to build "just in case" | Making fundamental architecture decisions (DB, API style) |
| Composition > Inheritance | You need to combine behaviors flexibly | Genuine is-a relationships with shallow hierarchies |
| Law of Demeter | Behavioral objects with encapsulation | Data objects, fluent APIs, functional chains |
| Separation of Concerns | Code grows beyond one person's comprehension | Tiny scripts or prototypes |
Principles That Can Conflict
| Principle A | Principle B | Conflict | Resolution |
|---|---|---|---|
| DRY | KISS | DRY abstraction makes code harder to read | If the duplication is simple and local, keep it |
| DRY | YAGNI | DRY requires building a shared abstraction | Wait for the Rule of Three |
| SoC | KISS | Separation adds more files and indirection | Separate only when complexity justifies it |
| OCP | YAGNI | OCP says design for extension; YAGNI says don't | Build the extension point when you need the 2nd extension |
9. Key Takeaways
- DRY is about knowledge, not code. Two functions with identical syntax but different purposes are NOT a DRY violation. Two different-looking functions encoding the same business rule ARE.
- KISS is the meta-principle. When in doubt, choose the simpler option. Cleverness is a liability in a codebase maintained by a team.
- YAGNI saves you from your future self. The features you anticipate needing are usually wrong. Build for today's requirements and refactor when real requirements emerge.
- Composition over Inheritance gives you flexibility. Compose small, focused behaviors instead of building deep class trees. Mixins, interfaces, and injection are your tools.
- Law of Demeter limits ripple effects. The fewer objects your code knows about, the fewer things can break when something changes.
- Separation of Concerns is about team scale. At small scale it's nice; at large scale it's essential. Separate along boundaries that change independently.
- Over-engineering is as dangerous as under-engineering. The right level of abstraction depends on team size, expected lifespan, change frequency, and domain complexity.
- Principles conflict. Learning when to prioritize one over another is the mark of experience.
10. Explain-It Challenge
Test your understanding by explaining these scenarios:
-
DRY debate: A teammate extracts two validation functions into one because they "look the same." One validates user signups, the other validates payment details. Both happen to check for non-empty strings and valid emails. Is this a good DRY extraction? Why or why not?
-
KISS vs Enterprise: Your tech lead wants to implement a full CQRS (Command Query Responsibility Segregation) pattern for a CRUD app with 100 users. Make the case for KISS.
-
YAGNI judgment call: You're building an API that currently serves a web app. The product roadmap shows a mobile app in 6 months. Should you build the mobile-specific endpoints now? Where does YAGNI apply and where doesn't it?
-
Composition challenge: You have
Printable,Scannable,Faxable, andCopyablecapabilities for office machines. Design a system using composition that lets you create a BasicPrinter (print only), MultiFunctionPrinter (all four), and ScannerOnly (scan only).