← Back to principles

Design Principle

SOLID Principles

Master SOLID principles (SRP, OCP, LSP, ISP, DIP) to design maintainable, testable software. Learn when to apply each principle and how they work together.

SOLID Principles

Why This Matters

SOLID is like traffic rules for code. You can drive without knowing the rules, but you'll cause accidents. Similarly, you can write code without SOLID, but you'll create systems that are hard to change, hard to test, and hard to understand.

This matters because code changes constantly. New features, bug fixes, refactoring—every change risks breaking something else. SOLID principles help you design code that's easier to change. When you need to add a new payment method, you shouldn't have to modify the core payment logic. When you need to change how emails are sent, you shouldn't have to touch user management code.

In interviews, when someone asks "How would you design this system?", they're testing whether you understand SOLID. Can you identify when code violates these principles? Can you refactor code to follow them? Most engineers can't. They write code that works, but it's hard to change.

What Engineers Usually Get Wrong

Engineers often think SOLID is a checklist—either your code follows it or it doesn't. But SOLID is a set of principles that work together to make code maintainable. You don't have to follow all of them all the time. Sometimes, violating one principle is the right choice if it makes the code simpler or more maintainable.

Engineers also apply SOLID too early. They create abstractions before they know what they need. It's better to start simple and refactor toward SOLID as you learn what changes. Premature abstraction is as bad as no abstraction.

How This Breaks Systems in the Real World

A service had a PaymentProcessor class that handled credit cards, PayPal, and bank transfers. Initially, this was one class with if/else statements. It worked fine. But as the team added more payment methods, the class grew to 500 lines with nested if/else statements. Adding a new payment method required modifying this large class, risking breaking existing methods.

The fix? Apply Open/Closed Principle—create a PaymentMethod interface, with implementations for each payment type. Now, adding a new payment method means creating a new class, not modifying existing code. The lesson? SOLID helps when code needs to change. Apply it when you see the need, not prematurely.

Another story: A service had a UserService class that handled user creation, user updates, email sending, password hashing, and database operations. This worked fine initially. But when the team needed to change how emails were sent (switch from SMTP to an email service API), they had to modify UserService. When they needed to change password hashing (switch algorithms), they had to modify UserService again. Each change risked breaking other functionality. The class became a bottleneck—everyone was afraid to touch it.

The fix? Apply Single Responsibility Principle—split into UserRepository (database operations), EmailService (email sending), PasswordHasher (password operations), and UserService (orchestration). Now, changes to email don't affect password hashing, and vice versa.


The SOLID principles help engineers design software that stays malleable as teams grow. Each principle is a conversation starter, not a strict rule.


🧩 S — Single Responsibility Principle (SRP)

A module should have one reason to change. Define boundaries by the decision they protect, not by line count.

1// ❌ Mixed responsibilities: persistence + messaging.
2class UserService {
3 createUser(input: CreateUserRequest) {
4 const user = this.repo.save(input);
5 this.emailer.sendWelcome(user.email);
6 }
7}
8
9// ✅ Separate causes of change: data vs. delivery.
10class UserService {
11 constructor(private readonly users: UserRepository) {}
12
13 createUser(input: CreateUserRequest) {
14 return this.users.save(input

Lesson: SRP isn't about fewer methods — it's about isolating causes of change so incidents stay contained.

What people usually get wrong: Most engineers think Single Responsibility means "a class should do one thing." But that's too vague. What is "one thing"? A better way to think about it is: "a class should have one reason to change." If you have to change a class for multiple reasons (e.g., to fix a bug in business logic AND to change how data is stored), the class has multiple responsibilities. Also, engineers often take this too far—they create a class for every tiny operation, leading to over-engineering. The key is balance: separate concerns that change for different reasons, but don't create unnecessary abstractions.


🔄 O — Open/Closed Principle (OCP)

Systems should be open for extension, closed for modification. Add behaviour through clear extension points, not increasingly fragile edits.

1// ❌ Editing core logic for every new plan.
2function renderInvoice(plan: "starter" | "pro" | "enterprise") {
3 if (plan === "starter") return /* ... */;
4 if (plan === "pro") return /* ... */;
5 if (plan === "enterprise") return /* ... */;
6}
7
8// ✅ Extend by registering a new strategy.
9interface PricingStrategy {
10 total(lineItems: LineItem[]): Money;
11}
12
13class Subscription {

Lesson: Strategy patterns or registries convert "edit the enum" into "add a module". Extension points buy you calm evolutions.

What people usually get wrong: Engineers often think OCP means "never modify existing code." But that's not practical. OCP is about designing extension points so that common changes (adding new payment methods, new pricing plans) don't require modifying core logic. The key is identifying what changes frequently and designing for that.


🤝 L — Liskov Substitution Principle (LSP)

Subtypes must honour the expectations of their supertypes. Violations show up as surprising runtime checks or instanceof escapes.

1// ❌ Subclass tightens contract by throwing unexpectedly.
2class Queue {
3 protected items: string[] = [];
4 enqueue(value: string) {
5 this.items.push(value);
6 }
7 dequeue() {
8 return this.items.shift();
9 }
10 size() {
11 return this.items.length;
12 }
13}
14
15class LimitedQueue

Lesson: If you find yourself tightening preconditions or relaxing postconditions, question whether inheritance was the right model.

What people usually get wrong: Engineers often think LSP means "subclasses must do everything the parent does." But LSP is about behavioral contracts. If code that works with a Queue breaks when you substitute a LimitedQueue, that's an LSP violation. The subclass should be usable anywhere the parent is used, without the caller knowing the difference.


🔁 I — Interface Segregation Principle (ISP)

Clients should not depend on methods they don't use. Lean interfaces keep mocks honest and avoid leaking surface area.

1// ❌ Inflated interface forces read-only clients to depend on writes.
2interface Storage {
3 put(key: string, value: string): Promise<void>;
4 get(key: string): Promise<string | null>;
5 delete(key: string): Promise<void>;
6}
7
8// ✅ Segment interfaces by capability.
9interface StorageWriter {
10 put(key: string, value: string): Promise

Lesson: Split read/write responsibilities or sync/async requirements. Segregated interfaces keep dependency graphs aligned to use cases.

What people usually get wrong: Engineers often create "god interfaces" that have everything. But if a client only needs to read data, why should it depend on write methods? This creates unnecessary coupling. Split interfaces by what clients actually need.


🧱 D — Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level implementation details. Invert via abstractions so policies lead, mechanisms follow.

1// ❌ High-level service constructs concrete gateway directly.
2class BillingService {
3 async bill(account: Account, invoice: Invoice) {
4 const gateway = new StripeGateway(process.env.STRIPE_KEY!);
5 return gateway.charge({ account, invoice });
6 }
7}
8
9// ✅ Depend on abstraction and inject implementation.
10interface PaymentGateway {
11 charge(request: ChargeRequest): Promise<ChargeResponse>;
12}
13
14class BillingServiceWithGateway

Lesson: DIP turns hard-coded adapters into pluggable contracts. When the API team changes authentication, you swap adapters instead of rewriting business logic.

What people usually get wrong: Engineers often think DIP means "use dependency injection frameworks." But DIP is about depending on abstractions, not concretions. The business logic shouldn't know about Stripe or PayPal—it should know about a PaymentGateway interface. This makes code more testable and flexible.


💡 Real-World Insight

At scale, responsibilities blend easily. SOLID provides a checklist to notice entanglements before they dominate on-call rotations.

  • Use SRP to define service boundaries.
  • Reach for OCP when roadmap volatility accelerates.
  • Watch for LSP violations when refactoring hierarchies.

Final thought: SOLID isn't about dogma. It's a vocabulary to describe painful coupling and a compass for finding better seams.

Failure Stories You'll Recognize

The God Class: A service had a BusinessLogic class with 2000 lines of code. It handled user management, order processing, payment processing, email sending, and reporting. Every feature request required modifying this class. Every change risked breaking other features. The class became a bottleneck. The fix? Split into smaller classes with single responsibilities.

The Abstraction That Coupled Everything: A team created a DataAccess abstraction that worked for both database and file system. This seemed flexible. But the abstraction was leaky—database-specific concepts (transactions, connections) leaked into the interface. When the team needed database-specific features, they had to work around the abstraction. The abstraction made the code harder to understand and use. The fix? Use separate interfaces for database and file system.

The Premature Optimization: A team was building a feature that needed to process 100 items. A developer optimized it to handle 1 million items using complex algorithms and data structures. The optimization made the code hard to understand and debug. But the feature never processed more than 100 items. The optimization was unnecessary and made the code worse. The fix? Simplify to handle 100 items. Optimize only if you actually need to handle more.

What Interviewers Are Really Testing

They want to hear you talk about SOLID as a set of tools for making code maintainable, not as rigid rules. Junior engineers say "follow SOLID." Senior engineers say "SOLID helps make code maintainable, but apply it when you see the need, not prematurely. Context matters."

When they ask "How would you refactor this code?", they're testing:

  • Can you identify violations of SOLID principles?
  • Can you refactor code to follow them?
  • Do you understand when to apply them and when not to?

Interview Questions

Beginner

Q: What does the Single Responsibility Principle mean? Give a simple example of violating it.

A: SRP states that a class or module should have only one reason to change. A violation would be a User class that handles both user data persistence and sending emails. If email service changes, you'd need to modify the User class even though it's not related to user data. The fix: separate into UserRepository (handles data) and EmailService (handles notifications).


Intermediate

Q: How would you refactor a payment processing system to follow the Open/Closed Principle? The system currently uses if-else chains to handle different payment methods (credit card, PayPal, bank transfer).

A: Create a PaymentStrategy interface with a processPayment(amount, details) method. Each payment method (CreditCardPayment, PayPalPayment, BankTransferPayment) implements this interface. The payment service accepts a strategy via dependency injection. To add a new payment method, you create a new strategy class without modifying existing code. This follows OCP: open for extension (new strategies), closed for modification (core payment service).


Senior

Q: Design a microservices architecture for an e-commerce platform where the order service needs to communicate with inventory, payment, and shipping services. How do SOLID principles apply at the service level? How would you handle eventual consistency and failure scenarios?

A:

  • SRP: Each service owns one domain (orders, inventory, payments, shipping)
  • OCP: Use event-driven architecture with event handlers that can be extended without modifying core services
  • LSP: Service contracts (APIs) must be substitutable - if you swap payment providers, the order service shouldn't break
  • ISP: Order service shouldn't depend on shipping-specific methods it doesn't use - use narrow, focused APIs
  • DIP: Order service depends on abstractions (event bus, service interfaces) not concrete implementations

For consistency: Use saga pattern or event sourcing. Order service publishes "OrderCreated" event. Inventory reserves items, payment processes charge, shipping creates label. If any step fails, compensating transactions roll back. Services communicate via events/API contracts, not direct dependencies.


  • SRP isolates causes of change - if a bug fix needs to happen in multiple places, extract shared knowledge
  • OCP enables extension without modification - use strategy patterns, plugins, or event handlers instead of if-else chains
  • LSP ensures subtypes honor supertype contracts - prefer composition over inheritance when contracts might diverge
  • ISP keeps interfaces lean - clients shouldn't depend on methods they don't use (split read/write interfaces)
  • DIP inverts dependencies - high-level modules depend on abstractions, not concrete implementations
  • SOLID principles work together: SRP defines boundaries, OCP enables evolution, LSP/ISP ensure safe substitution, DIP decouples layers
  • At scale, SOLID helps notice entanglements before they dominate on-call rotations
  • These are heuristics, not strict rules - use them as conversation starters to identify painful coupling

How InterviewCrafted Will Teach This

We'll teach this through real code problems, not abstract definitions. Instead of memorizing "SOLID stands for Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion," you'll learn through scenarios like "this class is hard to change—why?"

You'll see how SOLID principles help you recognize and fix problems. When an interviewer asks "how would you refactor this code?", you'll think about separation of concerns, extension points, and maintainability—not just "apply SOLID."

  • DRY, KISS & YAGNI - Complementary principles that work together with SOLID to create maintainable code. DRY eliminates duplication, KISS keeps code simple, and YAGNI prevents premature optimization.
  • Composition over Inheritance - Related to LSP, composition often provides better flexibility than inheritance when designing class hierarchies.
  • Separation of Concerns - Core principle that aligns with SRP, focusing on dividing code into distinct sections that handle specific responsibilities.
  • Design Patterns - SOLID principles guide the application of design patterns like Strategy (OCP), Adapter (DIP), and many others that help implement these principles in practice.
  • Object-Oriented Design - SOLID principles are fundamental to good object-oriented design, helping create flexible and maintainable class structures.

Key Takeaways

SRP isolates causes of change - if a bug fix needs to happen in multiple places, extract shared knowledge

OCP enables extension without modification - use strategy patterns, plugins, or event handlers instead of if-else chains

LSP ensures subtypes honor supertype contracts - prefer composition over inheritance when contracts might diverge

ISP keeps interfaces lean - clients shouldn't depend on methods they don't use (split read/write interfaces)

DIP inverts dependencies - high-level modules depend on abstractions, not concrete implementations

SOLID principles work together: SRP defines boundaries, OCP enables evolution, LSP/ISP ensure safe substitution, DIP decouples layers

At scale, SOLID helps notice entanglements before they dominate on-call rotations

These are heuristics, not strict rules - use them as conversation starters to identify painful coupling

Keep exploring

Principles work best in chorus. Pair this lesson with another concept and observe how your architecture conversations change.