Skip to main content

SOLID Principles and DRY: Complete Development Guide

ยท 10 min read
Anand Raja
Senior Software Engineer

๐ŸŽฏ What are SOLID Principles?โ€‹

SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin (Uncle Bob) in the early 2000s, building on work from the 1980s and 1990s.

The Five SOLID Principles:โ€‹

  1. S - Single Responsibility Principle (SRP)
  2. O - Open/Closed Principle (OCP)
  3. L - Liskov Substitution Principle (LSP)
  4. I - Interface Segregation Principle (ISP)
  5. D - Dependency Inversion Principle (DIP)

๐Ÿ“š Historical Background and Namingโ€‹

Single Responsibility Principle (SRP)โ€‹

  • Origin: Introduced by Tom DeMarco in 1979
  • Popularized by: Robert C. Martin
  • Why "Single Responsibility": Each class should have only one reason to change, meaning one responsibility

Open/Closed Principle (OCP)โ€‹

  • Origin: Bertrand Meyer in 1988 in his book "Object-Oriented Software Construction"
  • Why "Open/Closed": Software entities should be open for extension but closed for modification

Liskov Substitution Principle (LSP)โ€‹

  • Origin: Barbara Liskov in a 1987 paper titled "Data Abstraction and Hierarchy"
  • Named after: Barbara Liskov, computer scientist and Turing Award winner
  • Why "Substitution": Objects of a superclass should be substitutable with objects of subclasses without breaking functionality

Interface Segregation Principle (ISP)โ€‹

  • Origin: Robert C. Martin in the 1990s
  • Why "Segregation": Clients shouldn't be forced to depend on interfaces they don't use - interfaces should be segregated (separated)

Dependency Inversion Principle (DIP)โ€‹

  • Origin: Robert C. Martin in 1996
  • Why "Inversion": Traditional dependency flow (high-level โ†’ low-level) is inverted - both depend on abstractions

๐Ÿ”„ What Does Dependency Inversion Really Mean?โ€‹

Traditional Dependency Flow (Without DIP):โ€‹

High-Level Module โ†’ Low-Level Module
(Business Logic) โ†’ (Database Implementation)

Inverted Dependency Flow (With DIP):โ€‹

High-Level Module โ†’ Abstraction โ† Low-Level Module
(Business Logic) โ†’ (Interface) โ† (Database Implementation)

The "Inversion" Explained:โ€‹

// โŒ Traditional Flow: High-level depends on low-level
class OrderService { // High-level
private database: MySQLDatabase; // Depends on concrete low-level

constructor() {
this.database = new MySQLDatabase(); // Direct dependency
}
}

// โœ… Inverted Flow: Both depend on abstraction
interface Database { // Abstraction
save(data: any): void;
}

class OrderService { // High-level
constructor(private database: Database) {} // Depends on abstraction
}

class MySQLDatabase implements Database { // Low-level depends on abstraction
save(data: any): void {
// Implementation
}
}

The dependency arrow is "inverted" - instead of high-level modules depending directly on low-level modules, both depend on abstractions.


๐Ÿ’ป SOLID Principles in JavaScript/TypeScriptโ€‹

Are SOLID Principles Applicable to JavaScript/TypeScript?โ€‹

Absolutely YES! Here's why and how:

1. JavaScript/TypeScript Support:โ€‹

// โœ… SRP in JavaScript
class UserValidator {
static validateEmail(email) {
return email && email.includes('@');
}
}

class UserService {
createUser(userData) {
if (!UserValidator.validateEmail(userData.email)) {
throw new Error('Invalid email');
}
// Create user logic
}
}
// โœ… OCP in TypeScript
interface PaymentMethod {
process(amount: number): void;
}

class PaymentProcessor {
private methods = new Map<string, PaymentMethod>();

addMethod(type: string, method: PaymentMethod) {
this.methods.set(type, method);
}
}

2. TypeScript Advantages for SOLID:โ€‹

// TypeScript interfaces enable better SOLID compliance
interface Readable {
read(id: string): any;
}

interface Writable {
write(data: any): void;
}

// ISP: Segregated interfaces
class ReadOnlyService implements Readable {
read(id: string): any {
return `Data for ${id}`;
}
// Not forced to implement write()
}

class ReadWriteService implements Readable, Writable {
read(id: string): any {
return `Data for ${id}`;
}

write(data: any): void {
console.log('Writing data');
}
}

3. Dependency Injection in JavaScript/TypeScript:โ€‹

// DIP with dependency injection
class EmailService {
send(message: string, recipient: string): void {
console.log(`Sending email to ${recipient}: ${message}`);
}
}

class NotificationService {
constructor(private emailService: EmailService) {}

notify(message: string, recipient: string): void {
this.emailService.send(message, recipient);
}
}

// Usage
const emailService = new EmailService();
const notificationService = new NotificationService(emailService);

๐Ÿ—๏ธ Programming Languages and SOLID Principlesโ€‹

Languages Where SOLID is Most Critical:โ€‹

LanguageSOLID ImportanceReason
Javaโญโญโญโญโญ Very HighStrong typing, large enterprise applications, OOP-focused
C#โญโญโญโญโญ Very HighEnterprise development, .NET ecosystem, strong OOP
C++โญโญโญโญ HighComplex systems, manual memory management
TypeScriptโญโญโญโญ HighLarge-scale applications, strong typing benefits
Pythonโญโญโญ Medium-HighDuck typing makes some principles less critical
JavaScriptโญโญโญ MediumDynamic nature, but still valuable for large apps
Goโญโญโญ MediumInterface-based design, but simpler OOP model
Rustโญโญโญ MediumTrait system supports principles, but ownership model differs

Why Some Languages Need SOLID More:โ€‹

Java/C# - Highest Need:โ€‹

// Java enterprise applications benefit greatly from SOLID
public interface PaymentProcessor {
boolean processPayment(BigDecimal amount, PaymentDetails details);
}

@Service
public class OrderService {
private final PaymentProcessor paymentProcessor;
private final NotificationService notificationService;

public OrderService(PaymentProcessor paymentProcessor,
NotificationService notificationService) {
this.paymentProcessor = paymentProcessor;
this.notificationService = notificationService;
}
}

Python - Medium Need:โ€‹

# Python's duck typing makes LSP less critical
class Bird:
def move(self):
pass

class Duck(Bird):
def move(self):
print("Flying and swimming")

class Penguin(Bird):
def move(self):
print("Swimming and walking")

# Both work without explicit interfaces
def make_bird_move(bird):
bird.move() # Works with any object that has move()

JavaScript - Growing Need:โ€‹

// Modern JavaScript applications benefit from SOLID
class ApiService {
constructor(httpClient, logger) {
this.httpClient = httpClient;
this.logger = logger;
}

async fetchData(url) {
this.logger.log(`Fetching data from ${url}`);
return await this.httpClient.get(url);
}
}

// Easy to test and maintain
const apiService = new ApiService(mockHttpClient, mockLogger);

โœ… Where to Use SOLID Principlesโ€‹

1. Large Enterprise Applicationsโ€‹

// Enterprise order management system
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order>;
}

interface PaymentGateway {
charge(amount: number, token: string): Promise<PaymentResult>;
}

interface NotificationService {
sendOrderConfirmation(order: Order): Promise<void>;
}

class OrderProcessingService {
constructor(
private orderRepo: OrderRepository,
private paymentGateway: PaymentGateway,
private notificationService: NotificationService
) {}

async processOrder(orderData: OrderData): Promise<string> {
// Complex business logic with multiple dependencies
const order = new Order(orderData);
await this.orderRepo.save(order);

const payment = await this.paymentGateway.charge(
order.total,
orderData.paymentToken
);

await this.notificationService.sendOrderConfirmation(order);
return order.id;
}
}

2. Microservices Architectureโ€‹

// User service following SOLID principles
interface UserRepository {
findById(id: string): Promise<User>;
save(user: User): Promise<void>;
}

interface EventPublisher {
publish(event: DomainEvent): Promise<void>;
}

class UserService {
constructor(
private userRepo: UserRepository,
private eventPublisher: EventPublisher
) {}

async updateUser(id: string, updates: UserUpdates): Promise<void> {
const user = await this.userRepo.findById(id);
user.update(updates);
await this.userRepo.save(user);

await this.eventPublisher.publish(
new UserUpdatedEvent(user.id, updates)
);
}
}

3. Libraries and Frameworksโ€‹

// HTTP client library following OCP
interface HttpInterceptor {
intercept(request: HttpRequest): HttpRequest;
}

class HttpClient {
private interceptors: HttpInterceptor[] = [];

addInterceptor(interceptor: HttpInterceptor): void {
this.interceptors.push(interceptor);
}

async get(url: string): Promise<Response> {
let request = new HttpRequest(url);

// Apply all interceptors
for (const interceptor of this.interceptors) {
request = interceptor.intercept(request);
}

return await fetch(request.url, request.options);
}
}

// Easy to extend with new interceptors
class AuthInterceptor implements HttpInterceptor {
intercept(request: HttpRequest): HttpRequest {
request.headers['Authorization'] = 'Bearer token';
return request;
}
}

4. Testing Scenariosโ€‹

// SOLID principles make testing easier
interface EmailProvider {
send(to: string, subject: string, body: string): Promise<void>;
}

class UserRegistrationService {
constructor(
private userRepo: UserRepository,
private emailProvider: EmailProvider
) {}

async registerUser(userData: UserData): Promise<void> {
const user = new User(userData);
await this.userRepo.save(user);
await this.emailProvider.send(
user.email,
'Welcome!',
'Welcome to our platform'
);
}
}

// Easy to test with mocks
describe('UserRegistrationService', () => {
it('should register user and send welcome email', async () => {
const mockUserRepo = {
save: jest.fn()
};
const mockEmailProvider = {
send: jest.fn()
};

const service = new UserRegistrationService(
mockUserRepo,
mockEmailProvider
);

await service.registerUser({ email: 'test@example.com' });

expect(mockUserRepo.save).toHaveBeenCalled();
expect(mockEmailProvider.send).toHaveBeenCalledWith(
'test@example.com',
'Welcome!',
'Welcome to our platform'
);
});
});

โŒ Where NOT to Use SOLID Principlesโ€‹

1. Simple Scripts and Utilitiesโ€‹

// โŒ OVER-ENGINEERING: Don't use SOLID for simple scripts
// Simple file processing script
const fs = require('fs');

// This is fine - no need for abstraction
function processLogFile(filename) {
const content = fs.readFileSync(filename, 'utf8');
const lines = content.split('\n');
const errors = lines.filter(line => line.includes('ERROR'));

console.log(`Found ${errors.length} errors`);
errors.forEach(error => console.log(error));
}

processLogFile('app.log');

2. Rapid Prototypingโ€‹

// โŒ OVER-ENGINEERING: Don't use SOLID for prototypes
// Quick prototype for testing an idea
function quickWeatherApp() {
// Direct API call - no abstraction needed
fetch('https://api.weather.com/current')
.then(response => response.json())
.then(data => {
document.getElementById('weather').innerHTML =
`Temperature: ${data.temp}ยฐC`;
});
}

// This is perfectly fine for a prototype

3. Configuration Filesโ€‹

// โŒ OVER-ENGINEERING: Simple config doesn't need SOLID
// Application configuration
const config = {
database: {
host: 'localhost',
port: 5432,
name: 'myapp'
},
api: {
timeout: 5000,
retries: 3
}
};

module.exports = config;

4. Small Pure Functionsโ€‹

// โŒ OVER-ENGINEERING: Pure utility functions don't need SOLID
// Simple utility functions
function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
}

function calculateTax(price, rate) {
return price * rate;
}

function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

// These are fine as-is

5. One-off Data Processingโ€‹

# โŒ OVER-ENGINEERING: One-time data migration script
import csv
import json

# Simple data conversion - no need for classes/interfaces
with open('users.csv', 'r') as csv_file:
reader = csv.DictReader(csv_file)
users = list(reader)

with open('users.json', 'w') as json_file:
json.dump(users, json_file, indent=2)

print(f"Converted {len(users)} users to JSON")

๐Ÿ“Š When to Apply SOLID: Decision Matrixโ€‹

ScenarioCode SizeTeam SizeMaintenance PeriodApply SOLID?
Enterprise AppLarge (10K+ lines)Large (5+ devs)Yearsโœ… YES
Startup MVPMedium (1K-5K lines)Small (2-3 devs)Months๐Ÿค” MAYBE
Personal ProjectSmall (<1K lines)SoloWeeksโŒ NO
Library/FrameworkAny sizeAny sizeLong-termโœ… YES
Data ScriptSmallSoloOne-timeโŒ NO
MicroserviceMediumMedium (3-5 devs)Long-termโœ… YES
PrototypeAny sizeAny sizeDaysโŒ NO

๐ŸŽฏ Gradual Adoption Strategyโ€‹

Phase 1: Start Simpleโ€‹

// Begin with working code
class OrderService {
async createOrder(orderData: any) {
// Validation
if (!orderData.items || orderData.items.length === 0) {
throw new Error('Order must have items');
}

// Save to database
const order = await database.save(orderData);

// Send notification
await emailService.send(orderData.customerEmail, 'Order created');

return order;
}
}

Phase 2: Extract Responsibilities (SRP)โ€‹

// Extract validation
class OrderValidator {
static validate(orderData: any): void {
if (!orderData.items || orderData.items.length === 0) {
throw new Error('Order must have items');
}
}
}

class OrderService {
async createOrder(orderData: any) {
OrderValidator.validate(orderData);

const order = await database.save(orderData);
await emailService.send(orderData.customerEmail, 'Order created');

return order;
}
}

Phase 3: Add Abstractions (DIP)โ€‹

// Add interfaces when you need flexibility
interface OrderRepository {
save(orderData: any): Promise<Order>;
}

interface NotificationService {
send(email: string, message: string): Promise<void>;
}

class OrderService {
constructor(
private orderRepo: OrderRepository,
private notificationService: NotificationService
) {}

async createOrder(orderData: any) {
OrderValidator.validate(orderData);

const order = await this.orderRepo.save(orderData);
await this.notificationService.send(
orderData.customerEmail,
'Order created'
);

return order;
}
}

Phase 4: Enable Extension (OCP)โ€‹

// Add plugin system when needed
interface OrderProcessor {
process(order: Order): Promise<void>;
}

class InventoryProcessor implements OrderProcessor {
async process(order: Order): Promise<void> {
// Reserve inventory
}
}

class PaymentProcessor implements OrderProcessor {
async process(order: Order): Promise<void> {
// Process payment
}
}

class OrderService {
private processors: OrderProcessor[] = [];

addProcessor(processor: OrderProcessor): void {
this.processors.push(processor);
}

async createOrder(orderData: any) {
const order = await this.orderRepo.save(orderData);

// Process with all registered processors
for (const processor of this.processors) {
await processor.process(order);
}

return order;
}
}

๐Ÿ”‘ Key Principles Summaryโ€‹

SOLID + DRY Guidelines:โ€‹

  1. Start simple - Don't over-engineer from the beginning
  2. Refactor when you feel pain - Code duplication, hard to test, hard to change
  3. Apply gradually - One principle at a time
  4. Focus on value - Principles should solve real problems
  5. Team size matters - Larger teams benefit more from SOLID
  6. Maintenance duration matters - Long-term projects need SOLID more
  7. JavaScript/TypeScript fully support SOLID - Especially with TypeScript's type system
  8. Enterprise languages (Java/C#) benefit most - But all OOP languages can use SOLID

The Bottom Line:โ€‹

SOLID principles are tools, not rules. Use them when they add value, not because you should. Start with working code, then refactor when complexity grows. The goal is maintainable, testable, and flexible code - not perfect adherence to principles.

JavaScript and TypeScript are excellent languages for applying SOLID principles, especially in larger applications where the benefits of good architecture become apparent. The dynamic nature of JavaScript doesn't prevent good design - it just requires more discipline.