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.
July 31, 2019The Singleton pattern ensures a class has only one instance and provides a global point of access to it. It is one of the most widely used — and misused — design patterns in software engineering. When applied correctly, it solves real problems around shared resource management, such as configuration settings, connection pools, and logging systems.
Imagine your application needs a centralized configuration manager that reads settings from a file or environment variables. If every part of your codebase creates its own instance, you end up with duplicated I/O, inconsistent state, and wasted memory. Worse, if one module changes a setting in its copy, other modules never see the update. You need a single, shared source of truth.
The Singleton pattern solves this by restricting instantiation to a single object. The class itself controls its own creation, stores the instance privately, and exposes it through a static access method. Every caller gets the same instance, guaranteeing consistent state across the entire application.
Controlled access to a sole instance. The class owns its lifecycle — no external code can create or destroy additional instances. This gives you a single coordination point without relying on global variables.
This example implements a thread-safe ConfigurationManager that loads application settings once and provides them everywhere.
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class ConfigurationManager {
// volatile ensures visibility across threads
private static volatile ConfigurationManager instance;
private final Properties properties;
// Private constructor prevents external instantiation
private ConfigurationManager() {
properties = new Properties();
try (FileInputStream fis = new FileInputStream("app.properties")) {
properties.load(fis);
System.out.println("Configuration loaded from app.properties");
} catch (IOException e) {
System.err.println("Failed to load config: " + e.getMessage());
// Load defaults
properties.setProperty("db.host", "localhost");
properties.setProperty("db.port", "5432");
properties.setProperty("app.name", "MyApplication");
}
}
// Double-checked locking for thread safety
public static ConfigurationManager getInstance() {
if (instance == null) {
synchronized (ConfigurationManager.class) {
if (instance == null) {
instance = new ConfigurationManager();
}
}
}
return instance;
}
public String get(String key) {
return properties.getProperty(key);
}
public String get(String key, String defaultValue) {
return properties.getProperty(key, defaultValue);
}
public int getInt(String key, int defaultValue) {
String value = properties.getProperty(key);
if (value == null) return defaultValue;
try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException e) {
return defaultValue;
}
}
}
// Usage
public class Application {
public static void main(String[] args) {
ConfigurationManager config = ConfigurationManager.getInstance();
String dbHost = config.get("db.host", "localhost");
int dbPort = config.getInt("db.port", 5432);
String appName = config.get("app.name", "DefaultApp");
System.out.println("App: " + appName);
System.out.println("DB: " + dbHost + ":" + dbPort);
// Same instance everywhere
ConfigurationManager sameConfig = ConfigurationManager.getInstance();
System.out.println("Same instance: " + (config == sameConfig)); // true
}
}
The same configuration manager scenario in Python, using a class-level instance with thread safety.
import threading
import json
import os
class ConfigurationManager:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None:
with cls._lock:
# Double-checked locking
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self._initialized = True
self._config = {}
self._load_config()
def _load_config(self):
config_path = "config.json"
if os.path.exists(config_path):
with open(config_path, "r") as f:
self._config = json.load(f)
print("Configuration loaded from config.json")
else:
# Load defaults
self._config = {
"db.host": "localhost",
"db.port": 5432,
"app.name": "MyApplication",
}
print("Loaded default configuration")
def get(self, key, default=None):
return self._config.get(key, default)
def get_int(self, key, default=0):
value = self._config.get(key)
if value is None:
return default
try:
return int(value)
except (ValueError, TypeError):
return default
# Usage
if __name__ == "__main__":
config = ConfigurationManager()
db_host = config.get("db.host", "localhost")
db_port = config.get_int("db.port", 5432)
app_name = config.get("app.name", "DefaultApp")
print(f"App: {app_name}")
print(f"DB: {db_host}:{db_port}")
# Same instance everywhere
same_config = ConfigurationManager()
print(f"Same instance: {config is same_config}") # True
Runtime.getRuntime() returns the single Runtime instance for the JVM.logging.getLogger(name) returns the same logger instance for a given name across the entire application.Good fit
heavy read
Bad fit
Ad hoc queries
Updates
Joins
Social Applications
Social applications can go from 0 users to a million users in a very short time. The best DB fit for these kinds of applications is the DB that can easily scale horizontally which is where MongoDB comes into the picture.
Online Advertisement
Ads can be saved and updated from all kinds of sources. Data is not really related to each other but there is a lot of them.
Data Archive
Archiving a lot of data that may or may not be retrieved often can be done with a NoSQL DB like MongoDB.
Performance
MongoDB tends to be faster than relational DB when it comes to retrieving data. NoSQL database scales horizontally and is designed to scale to hundreds of millions and even billions of users doing updates as well as reads
Relational DB tends to be faster on ad hoc queries, updates, and joins. But applications can be designed and developed in a way to avoid these use cases. For example: to update you can use primary keys to update.
Differences
NoSQL scales horizontally (add more servers) but SQL Relational DB scales vertically (add more memories).
SQL relational DB must know columns and data types of the data to store. NoSQL DB does not have to know columns and data types. A row in a SQL relational DB contains all columns even if they are empty. A row in a NoSQL DB may or may not have all columns.
Advantages
Big data.
Schema changes are problematic and time-consuming in the world of SQL. NoSQL is great when business requirements are not stable and keep changing. You don’t have to modify or alter table and column definitions which is a painful process.
Scalability.
Disadvantages