Standard Design Principles

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.

1. DRY (Don’t Repeat Yourself)

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:

  • Repeated values become constants
  • Repeated logic becomes a shared method
  • Repeated patterns become a base class or utility

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)

2. Encapsulation

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.

3. Composition Over Inheritance

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:

  • You need behavior from multiple sources (most languages allow only single inheritance)
  • The relationship is “has-a” rather than “is-a”
  • You want to swap behavior at runtime
# 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.

4. Program to Interfaces, Not Implementations

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.

5. Delegation

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.

Summary

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.




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *