Chapter 01 · Article 02 of 55
Software Design Principles
Software Design Principles are fundamental guidelines that help developers write code that is maintainable, flexible, and robust. These principles exist independently of any spe…
Article outline16 sections on this page+
Overview
Software Design Principles are fundamental guidelines that help developers write code that is maintainable, flexible, and robust. These principles exist independently of any specific design pattern - they are the "why" behind good design decisions. Mastering these principles allows you to evaluate trade-offs and make informed choices even in unfamiliar problem domains.
Core Principles at a Glance
| Principle | One-Liner | Key Benefit |
|---|---|---|
| DRY | Don't Repeat Yourself | Reduces maintenance burden |
| KISS | Keep It Simple, Stupid | Reduces cognitive load |
| YAGNI | You Aren't Gonna Need It | Prevents over-engineering |
| SoC | Separation of Concerns | Enables independent changes |
| LoD | Law of Demeter | Reduces coupling |
| CoI | Composition over Inheritance | Increases flexibility |
| PoLA | Principle of Least Astonishment | Improves usability |
| Fail Fast | Detect errors early | Easier debugging |
DRY - Don't Repeat Yourself
Theory
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. Duplication leads to inconsistency - when you change one copy but forget the other.
Violation Example
CLASS OrderService:
METHOD calculateTotal(order):
total = 0
FOR item IN order.items:
total = total + (item.price * item.quantity)
tax = total * 0.18
RETURN total + tax
CLASS InvoiceService:
METHOD generateInvoice(order):
total = 0
FOR item IN order.items:
total = total + (item.price * item.quantity)
tax = total * 0.18 // Duplicated logic!
invoice.amount = total + tax
RETURN invoice
Correct Example
CLASS PricingCalculator:
METHOD calculateTotal(order):
subtotal = 0
FOR item IN order.items:
subtotal = subtotal + (item.price * item.quantity)
RETURN subtotal
METHOD calculateTax(amount, taxRate = 0.18):
RETURN amount * taxRate
METHOD calculateFinalAmount(order):
subtotal = calculateTotal(order)
tax = calculateTax(subtotal)
RETURN subtotal + tax
When DRY Goes Wrong
DRY is about knowledge duplication, not code duplication. Two pieces of code that look identical but represent different business concepts should NOT be merged.
Example: Shipping address validation and billing address validation
may have identical rules TODAY but change for different reasons.
Merging them creates accidental coupling.
KISS - Keep It Simple, Stupid
Theory
The simplest solution that works is usually the best. Complexity should be added only when justified by requirements, not by anticipated future needs.
Violation Example
// Over-engineered string reversal
CLASS StringReverser:
METHOD reverse(input):
charArray = input.toCharArray()
stack = new Stack()
FOR char IN charArray:
stack.push(char)
result = ""
WHILE NOT stack.isEmpty():
result = result + stack.pop()
RETURN result
Correct Example
CLASS StringReverser:
METHOD reverse(input):
RETURN input.reversed()
KISS in Design Decisions
| Scenario | Complex Approach | KISS Approach |
|---|---|---|
| Config management | Custom XML parser | Use standard config library |
| Object creation | Abstract Factory with 5 layers | Simple Factory or constructor |
| State management | Full State pattern | Enum + switch (if few states) |
| Notification | Event bus with middleware | Direct method call (if 1 subscriber) |
YAGNI - You Aren't Gonna Need It
Theory
Don't implement functionality until it's actually needed. Speculative features add complexity, increase testing burden, and often turn out to be wrong when the real requirement arrives.
Violation Example
// Building for "future" multi-currency support nobody asked for
CLASS Payment:
amount: Decimal
currency: Currency
exchangeRate: Decimal
baseCurrencyAmount: Decimal
conversionTimestamp: DateTime
conversionProvider: String
METHOD convertTo(targetCurrency):
// 50 lines of conversion logic
// that nobody needs right now
Correct Example
// Build what's needed now
CLASS Payment:
amount: Decimal
METHOD process():
// Process payment in default currency
YAGNI vs Planning Ahead
| Situation | Apply YAGNI | Plan Ahead |
|---|---|---|
| Feature nobody requested | ||
| Known upcoming requirement | ||
| "What if someday..." | ||
| Contractual obligation | ||
| Making code testable | ||
| Adding abstraction "just in case" |
Separation of Concerns (SoC)
Theory
Each module/class/function should address a single concern. A concern is a distinct aspect of functionality. Mixing concerns makes code harder to understand, test, and modify.
Violation Example
CLASS UserController:
METHOD createUser(request):
// Validation (Concern 1)
IF request.email IS empty:
THROW ValidationError
IF NOT isValidEmail(request.email):
THROW ValidationError
// Business Logic (Concern 2)
user = new User(request.email, request.name)
hashedPassword = hashPassword(request.password)
user.setPassword(hashedPassword)
// Persistence (Concern 3)
db.connect()
db.execute("INSERT INTO users VALUES (...)")
db.close()
// Notification (Concern 4)
emailService.send(user.email, "Welcome!")
RETURN user
Correct Example
CLASS UserValidator:
METHOD validate(request):
IF request.email IS empty OR NOT isValidEmail(request.email):
THROW ValidationError
CLASS UserService:
METHOD createUser(request):
validator.validate(request)
user = new User(request.email, request.name)
user.setPassword(passwordHasher.hash(request.password))
userRepository.save(user)
eventBus.publish(new UserCreatedEvent(user))
RETURN user
CLASS UserRepository:
METHOD save(user):
db.execute("INSERT INTO users VALUES (...)")
CLASS WelcomeEmailListener:
METHOD onUserCreated(event):
emailService.send(event.user.email, "Welcome!")
Layered Architecture (SoC Applied)
+-------------------+
| Presentation | Controllers, Views, API endpoints
+-------------------+
|
+-------------------+
| Business Logic | Services, Domain objects, Rules
+-------------------+
|
+-------------------+
| Data Access | Repositories, DAOs, ORM
+-------------------+
|
+-------------------+
| Infrastructure | Database, File system, External APIs
+-------------------+
Law of Demeter (LoD) - Principle of Least Knowledge
Theory
A method should only talk to its immediate friends:
- The object itself
- Objects passed as parameters
- Objects it creates
- Its direct component objects
Don't talk to strangers. Avoid chaining calls like a.getB().getC().doSomething().
Violation Example
CLASS OrderProcessor:
METHOD processOrder(order):
// Reaching deep into object graph - fragile!
city = order.getCustomer().getAddress().getCity()
taxRate = taxService.getRateForCity(city)
order.getCustomer().getWallet().deduct(order.getTotal() * (1 + taxRate))
Correct Example
CLASS OrderProcessor:
METHOD processOrder(order):
city = order.getShippingCity() // Order knows how to get this
taxRate = taxService.getRateForCity(city)
order.chargeCustomer(order.getTotal() * (1 + taxRate)) // Order delegates
CLASS Order:
METHOD getShippingCity():
RETURN customer.getShippingCity()
METHOD chargeCustomer(amount):
customer.charge(amount)
CLASS Customer:
METHOD getShippingCity():
RETURN address.getCity()
METHOD charge(amount):
wallet.deduct(amount)
LoD Trade-offs
| Pro | Con |
|---|---|
| Reduces coupling | May create many wrapper/delegate methods |
| Easier to refactor internals | Can feel verbose |
| Better encapsulation | Requires more thought upfront |
Composition over Inheritance
Theory
Favor composing objects (has-a) over inheriting behavior (is-a). Inheritance creates tight coupling between parent and child, makes hierarchies rigid, and can lead to the fragile base class problem.
Violation Example (Inheritance)
CLASS Animal:
METHOD move(): print("walking")
METHOD makeSound(): ABSTRACT
CLASS Bird EXTENDS Animal:
METHOD makeSound(): print("chirp")
// Problem: Not all birds fly, but Bird inherits walk from Animal
// What about Penguin? Duck that swims?
CLASS FlyingBird EXTENDS Bird:
METHOD move(): print("flying")
CLASS SwimmingBird EXTENDS Bird:
METHOD move(): print("swimming")
// What about a duck that both flies AND swims? Multiple inheritance mess.
Correct Example (Composition)
INTERFACE MovementBehavior:
METHOD move()
CLASS WalkBehavior IMPLEMENTS MovementBehavior:
METHOD move(): print("walking")
CLASS FlyBehavior IMPLEMENTS MovementBehavior:
METHOD move(): print("flying")
CLASS SwimBehavior IMPLEMENTS MovementBehavior:
METHOD move(): print("swimming")
CLASS Bird:
movements: List<MovementBehavior>
sound: SoundBehavior
CONSTRUCTOR(movements, sound):
this.movements = movements
this.sound = sound
METHOD performMovements():
FOR movement IN movements:
movement.move()
// Duck can fly AND swim
duck = new Bird([new FlyBehavior(), new SwimBehavior()], new QuackSound())
When to Use Inheritance vs Composition
| Use Inheritance When | Use Composition When |
|---|---|
| True "is-a" relationship | "Has-a" or "can-do" relationship |
| Shared behavior won't change | Behavior varies or is swappable |
| Shallow hierarchy (1-2 levels) | Deep hierarchies emerging |
| Framework requires it | Flexibility needed |
| Liskov Substitution holds | Multiple behaviors needed |
Principle of Least Astonishment (PoLA)
Theory
Software should behave in a way that least surprises the user (or developer). Method names should accurately describe what they do. Side effects should be obvious or absent.
Violation Example
CLASS UserService:
// Name says "get" but it also CREATES if not found - surprising!
METHOD getUser(id):
user = repository.findById(id)
IF user IS null:
user = new User(id, "default")
repository.save(user)
emailService.sendWelcome(user) // Side effect!
RETURN user
Correct Example
CLASS UserService:
METHOD getUser(id):
RETURN repository.findById(id) // Returns null if not found
METHOD getOrCreateUser(id):
user = repository.findById(id)
IF user IS null:
user = createDefaultUser(id)
RETURN user
METHOD createDefaultUser(id):
user = new User(id, "default")
repository.save(user)
eventBus.publish(new UserCreatedEvent(user))
RETURN user
Fail Fast Principle
Theory
If something is going to fail, it should fail immediately and visibly rather than continuing in a corrupted state. Validate inputs at boundaries, check preconditions, and throw meaningful errors early.
Example
CLASS BankAccount:
METHOD withdraw(amount):
// Fail fast - validate immediately
IF amount <= 0:
THROW InvalidAmountError("Amount must be positive: " + amount)
IF amount > this.balance:
THROW InsufficientFundsError("Balance: " + balance + ", Requested: " + amount)
this.balance = this.balance - amount
RETURN new Transaction(amount, "WITHDRAWAL")
Principles Comparison Matrix
| Principle | Prevents | Costs | Tension With |
|---|---|---|---|
| DRY | Inconsistency, maintenance burden | May create premature abstractions | KISS (sometimes DRY adds complexity) |
| KISS | Over-engineering, cognitive overload | May limit extensibility | Open-Closed Principle |
| YAGNI | Wasted effort, unused code | May require refactoring later | Planning ahead |
| SoC | Tangled code, ripple effects | More files/classes | KISS (more indirection) |
| LoD | Tight coupling, fragile code | More delegate methods | Convenience |
| Composition > Inheritance | Rigid hierarchies, fragile base class | More objects to manage | Simplicity for true "is-a" |
| PoLA | Bugs from unexpected behavior | Requires more thought in naming | Clever optimizations |
| Fail Fast | Silent corruption, hard-to-debug errors | More validation code | Graceful degradation |
Constraints & Edge Cases
- DRY taken too far → Premature abstraction that couples unrelated concepts
- KISS taken too far → No abstraction at all, leading to rigid code
- YAGNI taken too far → No extensibility points, expensive refactoring later
- SoC taken too far → Too many tiny classes, hard to follow the flow
- LoD taken too far → Explosion of wrapper methods, "middle man" smell
The art of design is knowing when to apply each principle and how much.
Interview Follow-ups
Q&A Style
Q: Can DRY and KISS conflict? Give an example.
A: Yes. Removing duplication (DRY) sometimes requires creating abstractions (base classes, utility functions, generics) that add complexity, violating KISS. Example: Two services have similar but not identical validation logic. DRYing them into a shared generic validator with configuration options may be more complex than keeping two simple, independent validators. The right choice depends on how likely the logic is to diverge further.
Q: How do you decide when YAGNI applies vs when you should plan ahead?
A: YAGNI applies to features and concrete implementations. Planning ahead applies to structure - using interfaces, keeping classes focused, and avoiding tight coupling. You don't build the multi-currency system, but you don't hardcode "$" everywhere either. The test: "Would adding this later require changing existing code?" If yes, add a minimal abstraction point now.
Hints for Self-Practice
Q: You're building a notification system. The PM says "just email for now, but we might add SMS and push later." How do you apply these principles?
- Think about which principle says "don't build SMS now"
- Think about which principle says "don't hardcode EmailService everywhere"
- Consider what minimal abstraction enables future extension without building it now
- Think about the Open-Closed Principle connection
Q: A colleague argues that having 50 small classes violates KISS. How do you respond?
- Consider what "simple" means - simple to understand vs simple in structure
- Think about whether each class is independently simple
- Consider the alternative - fewer classes but each one complex
- Think about discoverability and navigation
Counter Questions to Ask Interviewer
- "Should I prioritize simplicity or extensibility for this design?"
- "How likely are the requirements to change in this area?"
- "Is this a prototype/MVP or production-grade system?"
- "Are there performance constraints that might override clean design?"
References & Whitepapers
- "The Pragmatic Programmer" by Hunt & Thomas - DRY principle origin (Chapter: "The Evils of Duplication")
- "Clean Architecture" by Robert C. Martin - Separation of Concerns, dependency rules
- "Design Patterns" (GoF, 1994) - "Favor object composition over class inheritance"
- "Law of Demeter" - Karl Lieberherr, 1987, Northeastern University
- "Agile Software Development" by Robert C. Martin - SOLID + supporting principles
- Paper: "On the Criteria To Be Used in Decomposing Systems into Modules" - David Parnas, 1972 (foundational paper on SoC and information hiding)