These are the foundational design principles every developer should internalize. They complement the SOLID Principles (covered in a dedicated post) and apply across languages and frameworks.
Every piece of knowledge should have a single, authoritative representation in your codebase. When you duplicate logic, you create multiple places that must change together — and they inevitably drift apart.
What to extract:
Warning: Don’t merge unrelated code just because it looks similar. Two functions that happen to share structure but serve different domains should stay separate. Premature abstraction is worse than duplication.
# BAD: duplicated discount logic
def get_member_price(price):
return price * 0.9
def get_employee_price(price):
return price * 0.9 # same now, but will diverge later? Keep separate.
# GOOD: truly shared logic extracted
def apply_discount(price, rate):
return price * (1 - rate)
member_price = apply_discount(100, 0.10)
employee_price = apply_discount(100, 0.15)
Hide internal state behind a well-defined interface. Make fields private, expose only what callers need, and keep mutation controlled. This protects invariants and lets you refactor internals without breaking consumers.
public class BankAccount {
private double balance;
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
public double getBalance() {
return balance;
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
balance += amount;
}
public void withdraw(double amount) {
if (amount > balance) throw new IllegalStateException("Insufficient funds");
balance -= amount;
}
}
Callers can’t set balance to a negative value directly. The class enforces its own rules. This is the core value of encapsulation — protecting invariants, not just hiding fields.
Inheritance creates tight coupling. A subclass is permanently bound to its parent’s implementation, and deep hierarchies become fragile. Composition lets you assemble behavior from smaller, independent components.
Prefer composition when:
# Composition: build behavior from small, focused components
class Engine:
def start(self):
return "Engine running"
class GPS:
def navigate(self, destination):
return f"Navigating to {destination}"
class Car:
def __init__(self):
self.engine = Engine()
self.gps = GPS()
def drive(self, destination):
return f"{self.engine.start()} | {self.gps.navigate(destination)}"
car = Car()
print(car.drive("New York"))
# Engine running | Navigating to New York
Swapping Engine for ElectricEngine requires no inheritance changes — just inject a different object.
Depend on abstractions. When your code references a concrete class, you can’t substitute alternatives without modifying callers. When it references an interface, any conforming implementation works.
// BAD: coupled to ArrayList
ArrayList<String> names = new ArrayList<>();
// GOOD: depends on the List interface
List<String> names = new ArrayList<>();
// This applies to method signatures too:
public void processOrders(List<Order> orders) {
// works with ArrayList, LinkedList, or any List implementation
}
This principle scales from variable declarations to entire service layers. In Spring, you inject OrderService (interface), not OrderServiceImpl.
A class shouldn’t do work that belongs to another class. If an object needs functionality that isn’t its core concern, delegate to a specialist. This keeps classes focused and testable.
// OrderController delegates to OrderService — it doesn't contain business logic
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/orders")
public ResponseEntity<Order> create(@RequestBody OrderRequest request) {
return ResponseEntity.ok(orderService.createOrder(request));
}
}
Controllers delegate to services. Services delegate to repositories. Each layer owns one concern. If you see a controller making JDBC calls, that’s a delegation failure.
These five principles — DRY, Encapsulation, Composition over Inheritance, Programming to Interfaces, and Delegation — form the day-to-day habits of effective developers. Combined with the SOLID Principles, they give you a strong foundation for writing clean, maintainable code in any language.