On the SOLID Principles
Why Is It Even Called SOLID?

(Robert C. Martin (Uncle Bob))
What Are the SOLID Principles?
Object-Oriented Programming (OOP) can, in fact, be written without using the SOLID Principles.
A program works perfectly well using only classes, inheritance or composition (composites), encapsulation, and polymorphism, even when the SOLID Principles are ignored entirely.
So why did the SOLID Principles emerge in the first place?
The SOLID Principles are a set of guidelines for Maintainability, Extensibility, and Coupling management.
In other words, the SOLID Principles are the guidelines for writing high-quality OOP code.

The Origins of the SOLID Principles
In 2001, a group of software developers gathered and published the Agile Manifesto. The core idea was simple: prioritize people and collaboration over heavy processes and documentation, and favor adapting to change over rigidly following a plan. As a philosophy, it was revolutionary, but it offered no answer to the question of "how, concretely, should code be written?" The manifesto pointed in a direction but did not describe a precise methodology for how to get there.

"Agile Software Development: Principles, Patterns, and Practices" (2002)
The following year, Robert C. Martin filled that gap in Agile Software Development: Principles, Patterns, and Practices (2002), translating the Agile "philosophy" into a code-level "methodology." From this book onward, Agile began to take hold not merely as theory but as a practical standard.

In the first edition, SRP, OCP, LSP, ISP, and DIP were introduced as individual principles. Each principle was not something Martin invented himself; rather, he selected and redefined concepts that were already scattered across the programming community, viewed through the lens of agile design.
OCP (the Open/Closed Principle) was first proposed by Bertrand Meyer in Object-Oriented Software Construction (1988), and LSP (the Liskov Substitution Principle) has its roots in the data abstraction paper Barbara Liskov presented at the 1987 OOPSLA Conference. SRP (the Single Responsibility Principle) is Martin's consolidation of David L. Parnas's concept of modular decomposition and Edsger W. Dijkstra's Separation of Concerns into a single principle. ISP (the Interface Segregation Principle) and DIP (the Dependency Inversion Principle) were derived by Martin directly through practical consulting work in the mid-1990s and published as essays in 1996. In short, SOLID is the result of gathering ideas that had emerged in different contexts over several decades and unifying them under a single, coherent design philosophy.
Later, Michael C. Feathers proposed the name SOLID by taking the first letter of each principle, and Martin adopted it in his blog posts and talks, giving it the form we know today. The fact that "solid" also means firm or sturdy makes the name an intentional pun. The term is generally thought to have become established around 2004.

Why SOLID Is Necessary: The Story of Design Smells
Before explaining what SOLID is, the question Martin first posed was this: "What is bad design?" He believed that understanding why a principle is necessary matters more than memorizing the principle itself. That is why Chapter 7 of the book begins not with the SOLID principles themselves, but with an examination of how code written without principles breaks down over time.
Rather than describing bad design in abstract terms, Martin classified it by the symptoms that actually appear in code. He called these Design Smells. A smell does not kill a system immediately, but left unaddressed, it causes rot. He identified four major smells.
Rigidity
This is the situation where you set out to fix one thing and end up having to change ten. It describes code that feels dangerous to touch, code that makes you sigh before you even start. Because pieces of the code are tightly bound together, a single small change triggers a cascade of further modifications.
Definition: A state in which software is difficult to change. When you try to modify one part, that change ripples into many other parts of the system, requiring far more work than anticipated.
Characteristics: Trying to modify a method in one class forces you to update dozens of other classes that call it. Changing a database schema requires touching the UI, business logic, and data access layers all at once.
Root cause: High Coupling and Low Cohesion.
Solution: Apply SRP (Single Responsibility Principle) and DIP (Dependency Inversion Principle) to reduce coupling between modules and minimize the impact that changes in one part have on others.
Fragility
If Rigidity is "hard to fix," Fragility is "you fixed it, and something unrelated broke." It occurs when developers cannot predict the scope of a change's impact. This is where the vicious cycle begins in which the more you modify the code, the more unstable the system becomes.
Definition: A state in which modifying one part causes unexpected problems or instability in other, unrelated parts of the system.
Characteristics: Bugs appear frequently after a code change, and errors surface in areas that have no direct connection to what was modified.
Examples: Modifying the payment module suddenly causes the login system to stop working. Improving the logic of one method produces a runtime error in another module that was using it.
Root cause: Improper use of inheritance (LSP violations) or failure to manage dependencies correctly.
Solution: Apply LSP (Liskov Substitution Principle) to ensure a stable inheritance structure, and apply ISP (Interface Segregation Principle) to eliminate unnecessary dependencies.
Immobility
You have a well-built module, but you cannot take it and use it in another project. You want to reuse it, but you cannot cut it free — that is immobility. The code is so tightly bound to a specific environment that there comes a point where the cost of separating it exceeds the cost of rewriting it from scratch.
Definition: A state in which a particular module or piece of code is difficult to isolate or impossible to reuse in a different project or context due to excessive dependencies.
Characteristics: The code is too tightly coupled to a specific environment, and reusing it requires large-scale modifications.
Examples: Database query logic is entangled with UI code, making it impossible to reuse only the query logic in another project. Code that depends on specific hardware does not run on a different platform.
Root cause: High coupling between modules and insufficient Abstraction.
Solution: Apply DIP (Dependency Inversion Principle) to design against abstractions rather than concrete implementations, and apply OCP (Open/Closed Principle) to create a reusable structure.
Viscosity
While the first three smells are problems inherent in the code itself, Viscosity has a somewhat different character. Design degradation is often not a matter of developer willpower; it happens because the environment steers people toward bad choices. When doing the right thing is harder than doing the wrong thing, people will inevitably choose the wrong thing.
Definition: A state in which the software development environment or the code itself makes work difficult and slow. High viscosity means that working the right way is harder than working the wrong way.
Characteristics: It takes two forms. Viscosity of the Software refers to situations where the code itself is hard to maintain or difficult to apply good design to, while Viscosity of the Environment refers to situations where inefficiencies in the development environment (builds, testing, deployment, etc.) slow down the pace of work.
Examples: It is easier to add code by copy-and-paste than by refactoring (Viscosity of the Software). Build times are so long that verifying changes after editing code becomes painfully slow (Viscosity of the Environment).
Root cause: Failure to follow design principles and overly complex development processes.
Solution: Use SRP to keep code simple, and reduce viscosity through continuous refactoring along with automated testing and build environments.
Martin used these four smells as diagnostic tools and presented SOLID as the prescription for them. The following summarizes which principle addresses which smell.
SRP reduces Rigidity in code.
OCP allows code to be extended without modifying existing code, which reduces Viscosity between code components.
LSP ensures the safety of inheritance hierarchies, thereby reducing Fragility.
DIP and ISP lower coupling, thereby reducing Immobility.
That said, SOLID is not the answer to every smell. A prime example is Shotgun Surgery. This anti-pattern, where a single change forces simultaneous modifications across multiple classes, is ironically made worse when SRP is applied too aggressively and responsibilities are scattered too broadly. SOLID is a powerful set of guidelines, but applying it mechanically without context does not eliminate smells; it merely replaces one smell with another. Ultimately, context matters most, and the most important conversation is about where the boundaries lie and how to agree on dividing responsibilities. It is also the hardest part.
Personally, the author believes that any teaching, when followed too blindly, becomes dogma, yet without going through that phase of blind adherence one cannot fully appreciate its dangers, so the experience itself is ultimately part of the process.

(Image source: https://www.instagram.com/techwithisha/reel/C1Ws1ZDt8_j/)
The SOLID Principles?
Before examining each SOLID principle, it is worth pausing to trace their philosophical roots. As early as 1974, Dijkstra had already expressed the ideas that would later become core concepts of SOLID:

"Let me try to explain to you, what to my taste is characteristic for all intelligent thinking. It is, that one is willing to study in depth an aspect of one's subject matter in isolation for the sake of its own consistency, all the time knowing that one is occupying oneself only with one of the aspects. We know that a program must be correct and we can study it from that viewpoint only; we also know that it should be efficient and we can study its efficiency on another day, so to speak. In another mood we may ask ourselves whether, and if so: why, the program is desirable. But nothing is gained --on the contrary!-- by tackling these various aspects simultaneously. It is what I sometimes have called "the separation of concerns", which, even if not perfectly possible, is yet the only available technique for effective ordering of one's thoughts, that I know of. This is what I mean by "focussing one's attention upon some aspect": it does not mean ignoring the other aspects, it is just doing justice to the fact that from this aspect's point of view, the other is irrelevant. It is being one- and multiple-track minded simultaneously.
— Edsger W. Dijkstra, EWD 447 (1974); *Selected Writings on Computing* (1982)
1. Single Responsibility Principle (SRP)
Proposed by: Robert C. Martin
Definition
- "A class should have only one responsibility." In other words, a class should have only one reason to change.
Meaning
- When a class takes on multiple roles, a change to one role affects the others, increasing both Rigidity and Fragility.
- Separating responsibilities simplifies code and makes maintenance easier.
Example
BadExample
using System;
using System.Data.SqlClient;
public class Employee_BadExample // Class name indicates `BadExample`
{
public string Name { get; set; }
public double BaseSalary { get; set; }
public string Department { get; set; }
private SqlConnection dbConnection; // The `Employee` class is responsible for database connection as well
public Employee_BadExample(string name, double baseSalary, string department, SqlConnection dbConnection)
{
Name = name;
BaseSalary = baseSalary;
Department = department;
this.dbConnection = dbConnection; // The `Employee` class receives a database connection object
}
public double CalculateSalary()
{
// Salary calculation method (assuming different bonus rates per department)
double bonusRate = 0;
if (Department == "Sales")
{
bonusRate = 0.1;
}
else if (Department == "Marketing")
{
bonusRate = 0.05;
}
return BaseSalary * (1 + bonusRate);
}
public void SaveToDatabase()
{
// Method for saving employee information to the database
double salary = CalculateSalary(); // Salary calculation logic resides inside `Employee`
try
{
dbConnection.Open();
SqlCommand command = new SqlCommand(
"INSERT INTO Employees (Name, Salary, Department) VALUES (@Name, @Salary, @Department)",
dbConnection);
command.Parameters.AddWithValue("@Name", Name);
command.Parameters.AddWithValue("@Salary", salary);
command.Parameters.AddWithValue("@Department", Department);
command.ExecuteNonQuery();
}
catch (Exception ex)
{
Console.WriteLine("Database save error: " + ex.Message);
}
finally
{
dbConnection.Close();
}
}
}
public class BadExample_Program // Main program class that uses `BadExample`
{
public static void Main(string[] args)
{
// Bad example: the `Employee_BadExample` class is taking on too many responsibilities
SqlConnection dbConn = null; // Replace with an actual database connection object (substituted with null here)
Employee_BadExample employee = new Employee_BadExample("홍길동", 3000000, "Sales", dbConn);
employee.SaveToDatabase(); // `Employee_BadExample` handles both salary calculation and database storage
}
}
Wrong case: The Employee class handles both salary calculation and database storage, so when the payroll logic changes, the DB code is affected as well.
GoodExample
using System;
using System.Data.SqlClient;
public class Employee // The Employee class is responsible only for data
{
public string Name { get; set; }
public double BaseSalary { get; set; }
public string Department { get; set; }
public Employee(string name, double baseSalary, string department)
{
Name = name;
BaseSalary = baseSalary;
Department = department;
}
}
public class SalaryCalculator // Class responsible for payroll calculation
{
public double CalculateSalary(Employee employee) // Takes an Employee object as a parameter
{
// Payroll calculation method
double bonusRate = 0;
if (employee.Department == "Sales")
{
bonusRate = 0.1;
}
else if (employee.Department == "Marketing")
{
bonusRate = 0.05;
}
return employee.BaseSalary * (1 + bonusRate);
}
}
public class EmployeeRepository // Class responsible for database storage
{
private SqlConnection dbConnection;
public EmployeeRepository(SqlConnection dbConnection)
{
this.dbConnection = dbConnection;
}
public void Save(Employee employee, double salary) // Takes an Employee object and the calculated salary
{
// Method that saves employee information to the database
try
{
dbConnection.Open();
SqlCommand command = new SqlCommand(
"INSERT INTO Employees (Name, Salary, Department) VALUES (@Name, @Salary, @Department)",
dbConnection);
command.Parameters.AddWithValue("@Name", employee.Name);
command.Parameters.AddWithValue("@Salary", salary);
command.Parameters.AddWithValue("@Department", employee.Department);
command.ExecuteNonQuery();
}
catch (Exception ex)
{
Console.WriteLine("Database save error: " + ex.Message);
}
finally
{
dbConnection.Close();
}
}
}
public class GoodExample_Program // Main program class using GoodExample
{
public static void Main(string[] args)
{
// Correct example: each class has a single responsibility
SqlConnection dbConn = null; // Replace with an actual database connection object
Employee employee = new Employee("김철수", 3500000, "Marketing");
SalaryCalculator calculator = new SalaryCalculator(); // Create object responsible for payroll calculation
double salary = calculator.CalculateSalary(employee);
EmployeeRepository repository = new EmployeeRepository(dbConn); // Create object responsible for database storage
repository.Save(employee, salary);
}
}
Correct case: Separate into SalaryCalculator and EmployeeRepository.
Benefit: Higher Cohesion and lower Coupling in the code.
Related Design Smells: Mitigates Rigidity and Viscosity.

(SRP explanation from Robert C. Martin's blog)
Historically, according to Robert C. Martin's blog, the concept began with David L. Parnas's modular decomposition and Edsger W. Dijkstra's term "Separation of Concerns,"
and was synthesized from the various notions of Coupling and Cohesion that had been circulating in the programming community.
As stated on Robert C. Martin's blog, the SRP is about people.
In practice, software changes in response to the needs of businesses and organizations, so the principle was designed to ensure that each module is responsible for only one business function,
making it easy to identify which team is responsible for changing that function.

2. Open/Closed Principle (OCP)
Proposed by: Bertrand Meyer
Definition
-"Software entities (classes, modules, etc.) should be open for extension, but closed for modification."
Meaning
-New functionality should be addable without modifying existing code.
-Implemented using Abstraction (interfaces, abstract classes) and polymorphism.
It is worth noting that the interpretation of OCP has shifted over time. When Meyer first proposed OCP in Chapter 2 of Object-Oriented Software Construction (1988), the implementation mechanism he envisioned was inheritance: extending behavior in a child class without modifying the parent class. Martin later reinterpreted the principle in terms of interfaces and polymorphism, making it possible to extend functionality simply by adding new implementations that depend on abstractions rather than concrete classes.
This is precisely why the GoodExample code below uses an interface.
Example
BadExample
using System;
public class PaymentProcessor_BadExample // Class name explicitly indicates BadExample
{
public void ProcessPayment(string paymentMethod, double amount)
{
// Method that handles processing based on payment type (heavy use of if-else statements)
if (paymentMethod == "Card")
{
// Credit card payment processing logic
Console.WriteLine($"Paid {amount} won via credit card");
}
else if (paymentMethod == "Cash")
{
// Cash payment processing logic
Console.WriteLine($"Paid {amount} won in cash");
}
else if (paymentMethod == "MobilePay") // When adding a new payment method, the code must be modified
{
// Mobile payment processing logic
Console.WriteLine($"Paid {amount} won via mobile payment");
}
else
{
Console.WriteLine("Unsupported payment method");
}
}
}
public class BadExample_Program // Main program class that uses BadExample
{
public static void Main(string[] args)
{
// Bad example: PaymentProcessor_BadExample violates OCP and cannot be extended easily
PaymentProcessor_BadExample processor = new PaymentProcessor_BadExample();
processor.ProcessPayment("Card", 10000);
processor.ProcessPayment("Cash", 5000);
processor.ProcessPayment("MobilePay", 7000); // Using a new payment method
}
}
Incorrect approach: Every time a new payment method (card, cash) is added to the PaymentProcessor class, an if-else condition must be modified.
GoodExample
using System;
// IPayment interface: a common interface applied to all payment methods
public interface IPayment
{
void ProcessPayment(double amount);
}
// Credit card payment class (implements IPayment)
public class CardPayment : IPayment
{
public void ProcessPayment(double amount)
{
Console.WriteLine($"[Credit Card Payment] Payment of {amount:N0} won completed");
}
}
// Cash payment class (implements IPayment)
public class CashPayment : IPayment
{
public void ProcessPayment(double amount)
{
Console.WriteLine($"[Cash Payment] Payment of {amount:N0} won completed");
}
}
// Mobile payment class (implements IPayment)
public class MobilePayPayment : IPayment
{
public void ProcessPayment(double amount)
{
Console.WriteLine($"[Mobile Payment] Payment of {amount:N0} won completed");
}
}
// Payment processing class: adheres to OCP
public class PaymentProcessor
{
private readonly IPayment paymentMethod;
// Injects the payment method via constructor (Dependency Injection can be applied)
public PaymentProcessor(IPayment paymentMethod)
{
this.paymentMethod = paymentMethod ?? throw new ArgumentNullException(nameof(paymentMethod));
}
public void Process(double amount)
{
paymentMethod.ProcessPayment(amount);
}
}
public class Program
{
public static void Main()
{
// Create each payment method object
var cardPayment = new CardPayment();
var cashPayment = new CashPayment();
var mobilePayPayment = new MobilePayPayment();
// OCP compliance: when a new payment method is added, there is no need to modify the PaymentProcessor code
var processor1 = new PaymentProcessor(cardPayment);
processor1.Process(10000);
var processor2 = new PaymentProcessor(cashPayment);
processor2.Process(5000);
var processor3 = new PaymentProcessor(mobilePayPayment);
processor3.Process(7000);
}
}
Correct approach: Create an IPayment interface and extend it with CardPayment and CashPayment classes.
Advantage: Maintains the stability of existing code while flexibly accommodating new requirements.
Related Design Smell: Viscosity mitigation

For reference, if you want a more detailed explanation, the five principles are thoroughly covered in Object-Oriented Software Construction (1988).
The Open/Closed Principle (OCP) appears in Part B, Chapter 3, "Modularity," of that book; readers who want further explanation are encouraged to look there.

(Data Abstraction and Hierarchy) (1987, OOPSLA Conference)
3. Liskov Substitution Principle (LSP)
Proposed by: Barbara Liskov
Definition
- "A subclass should be substitutable for its parent class without disrupting the behavior of the program."
- In other words, replacing a parent type with a child type anywhere in a program should cause no problems.
Meaning
- In an inheritance relationship, a child class must not violate the contract of its parent class.
- This is a principle for using polymorphism safely.
Example
public class Bird
{
public virtual void Fly() => Console.WriteLine("A bird is flying.");
}
public class Penguin : Bird
{
public override void Fly() // A penguin cannot fly
{
throw new NotImplementedException("A penguin cannot fly.");
}
}
public class Program
{
public static void MakeBirdFly(Bird bird)
{
bird.Fly(); // When a `Penguin` object is passed, an exception is thrown
}
static void Main()
{
Bird myBird = new Penguin();
MakeBirdFly(myBird); // the program may halt
}
}
Incorrect case: The Bird class has a Fly() method, and the Penguin subclass ignores it or throws an exception.
Good Example
// Abstracting `Bird` so it is not used directly
public abstract class Bird { }
// Define the flying interface
public interface IFlyable
{
void Fly();
}
// Sparrow class (implements the `IFlyable` interface)
public class Sparrow : Bird, IFlyable
{
public void Fly() => Console.WriteLine("The sparrow is flying.");
}
// Penguin class (does not implement the `IFlyable` interface, expressing that it cannot fly)
public class Penguin : Bird { }
public class Program
{
public static void MakeBirdFly(IFlyable bird)
{
bird.Fly();
}
static void Main()
{
IFlyable sparrow = new Sparrow();
MakeBirdFly(sparrow); // Runs successfully
}
}
Correct case: To separate FlyingBird (birds that can fly) from WalkingBird (birds that can only walk), the flying capability should be extracted into its own abstraction so that Penguin is never forced to implement Fly().
Advantage: Ensures stability and predictability in inheritance hierarchies.
Related Design Smell: Mitigates Fragility.
This principle is derived from a paper presented at the 1987 OOPSLA Conference,
in which Barbara Liskov addresses data abstraction and hierarchy, offering both philosophical and practical guidelines on what constitutes correct "inheritance" in object-oriented design.
Here, data abstraction refers to
hiding the internal implementation of data in a program and allowing access only through interfaces.
Ultimately, it comes down to how well you abstract real-world problems when modeling them.
Just as defining mammals as creatures with legs makes it difficult to classify whales as mammals,
the principle is best understood as a question of how effectively you can abstract a given problem.
4. Interface Segregation Principle (ISP)
Proposed by: Robert C. Martin
Definition
- "Clients should not be forced to depend on interfaces they do not use."
- In other words, interfaces should be separated into small, specific units.
Meaning
Instead of a large, general-purpose interface, design interfaces that provide only the functionality a client needs.
Reduces coupling by eliminating unnecessary dependencies
Example
BadExample
using System;
// IWorker_BadExample interface: contains too many features (ISP violation)
public interface IWorker_BadExample
{
void Work(); // Work feature
void Eat(); // Eat feature - but unnecessary for Robot
}
// Robot_BadExample class: because it implements IWorker_BadExample, it is forced to implement the unnecessary Eat() method
public class Robot_BadExample : IWorker_BadExample
{
public void Work()
{
Console.WriteLine("The robot is working diligently.");
}
public void Eat() // The robot does not need to eat, but must implement it
{
// Since the robot does not eat, it has no choice but to do nothing or throw an exception
Console.WriteLine("The robot cannot eat."); // Or throw new NotImplementedException();
}
}
// HumanWorker_BadExample class: implements IWorker_BadExample and correctly implements both Work() and Eat()
public class HumanWorker_BadExample : IWorker_BadExample
{
public void Work()
{
Console.WriteLine("The human is working diligently.");
}
public void Eat()
{
Console.WriteLine("The human is having lunch.");
}
}
public class BadExample_Program // Program class using BadExample
{
public static void Main(string[] args)
{
// Bad example: Robot_BadExample should not have an Eat() method
IWorker_BadExample robot = new Robot_BadExample();
robot.Work();
robot.Eat(); // It is awkward for the robot to call the Eat() method
IWorker_BadExample human = new HumanWorker_BadExample();
human.Work();
human.Eat();
}
}
Incorrect case: The IWorker interface includes both Work() and Eat(), forcing the Robot class to implement the unnecessary Eat() method.
GoodExample
using System;
// IWorkable interface: contains only the work functionality (ISP compliant)
public interface IWorkable
{
void Work(); // Work functionality
}
// IEatable interface: contains only the eating functionality (ISP compliant)
public interface IEatable
{
void Eat(); // Eating functionality
}
// Robot_GoodExample class: implements only the IWorkable interface (implements only the required functionality)
public class Robot_GoodExample : IWorkable // A robot can only work
{
public void Work()
{
Console.WriteLine("The robot performs tasks efficiently.");
}
// Eat() not implemented: a robot has no need to eat
}
// HumanWorker_GoodExample class: implements both IWorkable and IEatable interfaces (possesses all required functionality)
public class HumanWorker_GoodExample : IWorkable, IEatable // A human can both work and eat
{
public void Work()
{
Console.WriteLine("The human works creatively.");
}
public void Eat()
{
Console.WriteLine("The human is enjoying a delicious lunch.");
}
}
public class GoodExample_Program // Program class that uses GoodExample
{
public static void Main(string[] args)
{
// Correct example: Robot_GoodExample only needs to implement IWorkable
IWorkable robot = new Robot_GoodExample(); // The robot is used only as an IWorkable type
robot.Work();
// robot.Eat(); // Since Robot does not implement IEatable, calling Eat() is not allowed (compile error)
IWorkable humanWorker = new HumanWorker_GoodExample(); // HumanWorker can be used as an IWorkable type
humanWorker.Work();
IEatable humanEater = new HumanWorker_GoodExample(); // HumanWorker can also be used as an IEatable type
humanEater.Eat();
}
}
Correct case: Split into IWorkable and IEatable so that Robot implements only IWorkable.
Advantage: Increases code flexibility and reusability.
Associated Design Smells: Mitigates Fragility and Viscosity.

(DIP originated from a 1996 essay by Robert C. Martin)
5. Dependency Inversion Principle (DIP)
Proposed by: Robert C. Martin
Definition
- "High-level modules should not depend on low-level modules; both should depend on abstractions."
- Additionally, "depend on abstractions, not on concretions."
Meaning
- Place an interface or abstract class between modules to reduce coupling between them
- Implemented through Dependency Injection
Example
BadExample
using System;
// `SqlDatabase` class: a concrete database implementation (low-level module, depends directly on a specific database)
public class SqlDatabase_BadExample
{
public void Save(string data)
{
// Actual logic for saving data to `SqlDatabase` (implementation omitted)
Console.WriteLine($"`SqlDatabase` data save complete: {data}");
}
}
// `OrderService_BadExample` class: depends directly on `SqlDatabase` (DIP violation, high-level module)
public class OrderService_BadExample
{
private SqlDatabase_BadExample database; // Depends directly on the concrete `SqlDatabase` class
public OrderService_BadExample()
{
database = new SqlDatabase_BadExample(); // `OrderService` directly creates an instance of `SqlDatabase`
}
public void PlaceOrder(string orderData)
{
// Order processing logic (here, it simply saves the data)
Console.WriteLine($"Processing order: {orderData}");
database.Save(orderData); // `OrderService` directly calls the `Save()` method of `SqlDatabase`
Console.WriteLine("Order processing complete");
}
}
public class BadExample_Program // Program class using the bad example
{
public static void Main(string[] args)
{
// Bad example: `OrderService_BadExample` is tightly coupled to `SqlDatabase`, making extension difficult
OrderService_BadExample service = new OrderService_BadExample();
service.PlaceOrder("Customer: Hong Gil-dong, Product: Laptop");
}
}
Incorrect case: OrderService directly depends on SqlDatabase, so swapping the database requires modifying the code.
GoodExample
using System;
// DIP (Dependency Inversion Principle) compliance: `OrderService` depends only on the `IDatabase` interface
public interface IDatabase
{
void SaveOrder(string orderDetails);
}
// `SqlDatabase` implements the `IDatabase` interface
public class SqlDatabase : IDatabase
{
public void SaveOrder(string orderDetails)
{
Console.WriteLine($"[SqlDatabase] Order saved successfully: {orderDetails}");
}
}
// `MongoDatabase` implements the `IDatabase` interface (a new database type can be added)
public class MongoDatabase : IDatabase
{
public void SaveOrder(string orderDetails)
{
Console.WriteLine($"[MongoDatabase] Order saved successfully: {orderDetails}");
}
}
// `OrderService` depends on the interface (`IDatabase`) rather than a concrete implementation
public class OrderService
{
private readonly IDatabase database;
// DIP (Dependency Inversion Principle): `OrderService` depends on the interface rather than a concrete implementation
public OrderService(IDatabase database)
{
this.database = database; // Dependency Injection
}
public void PlaceOrder(string orderDetails)
{
database.SaveOrder(orderDetails); // Saves an order through the interface
}
}
// Because `OrderService` does not depend on a specific database, the database can be swapped out easily
public class Program
{
public static void Main()
{
// Creates objects directly without a DI container to inject dependencies
IDatabase sqlDatabase = new SqlDatabase();
IDatabase mongoDatabase = new MongoDatabase();
// `OrderService` using `SqlDatabase`
OrderService orderService1 = new OrderService(sqlDatabase);
orderService1.PlaceOrder("Order for Product A");
// `OrderService` using `MongoDatabase`
OrderService orderService2 = new OrderService(mongoDatabase);
orderService2.PlaceOrder("Order for Product B");
}
}
Correct case: Create an IDatabase interface, have OrderService depend on it, and provide SqlDatabase as the concrete implementation.
Benefit: Increases system flexibility and testability.
Related Design Smell: Alleviates Rigidity and Immobility.
The overall significance of the SOLID Principles is
to build software that is flexible and responsive by improving Maintainability and Extensibility.
The following is a concise summary of everything we have discussed so far.
S - Single Responsibility Principle (SRP) | A class should have only one responsibility. | Robert C. Martin (2002) |
O - Open/Closed Principle (OCP) | Software entities should be open for extension but closed for modification. | Bertrand Meyer (1988) |
L - Liskov Substitution Principle (LSP) | Subtypes must be substitutable for their base types. | Barbara Liskov (1987) |
I - Interface Segregation Principle (ISP) | Clients should not be forced to depend on interfaces they do not use. | Robert C. Martin (2002) |
D - Dependency Inversion Principle (DIP) | High-level modules should not depend on low-level modules. | Robert C. Martin (1996) |
But are the SOLID principles necessarily the right answer?
Can the SOLID principles truly be considered an unconditional answer?
Of course not. SOLID is merely a set of guidelines.

For example, the Actor class in Unreal Engine, currently the most widely used game engine,
handles physics, collision, lighting, mesh rendering, and countless other things.
In other words, a single class is doing far too much. Does splitting it up actually make things easier? Not at all. In fact, the cost of dividing it outweighs the benefit.
Actor is a core foundational class of Unreal Engine, and separating it to comply with SRP would affect the entire engine.
Does that make it a bad design? No.
Because games are designed around objects, this is a perfectly reasonable design rationale at that level of abstraction.
In fact, splitting it apart would impose enormous costs when re-compositing things per object.
The same goes for LSP. When a Button inherits from Widget in a GUI framework, it is expected to strictly preserve the parent's button behavior,
but this can actually become a practical problem. In cases where you need to design buttons with a wide variety of appearances, such a constraint can break design creativity.
Many GUI frameworks therefore accommodate such exceptions in practice. (Examples include the C++ Qt framework, GTK, and others.)
Furthermore, due to overhead introduced by interfaces and similar concerns, it is sometimes more appropriate to choose an alternative paradigm such as Data-Oriented Programming (DOP) rather than OOP itself.
SOLID is a solid set of guidelines for OOP design, but you also need to know when to clearly break them, given the performance limitations OOP can carry and the context of a given design.
How should you decide when it is acceptable to break SOLID?
SRP: When features share strong cohesion, strictly enforcing SRP can lead to frequent method calls across classes, introducing performance overhead.
OCP: Extensions are generally preferable, but when performance is the top priority, modifying existing code may be the better choice.
LSP: When an inheritance hierarchy is simple, there is no need to enforce it strictly.
ISP: Having too many interfaces can actually cause more confusion.
DIP: When abstraction introduces overhead, depending on a concrete implementation may be the better option.

Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live. Code for readability.
— John F. Woods (1991)
Closing Thoughts
The SOLID principles were born from Agile philosophy and have established themselves as powerful tools for achieving maintainability and extensibility, yet
they are not a "Silver Bullet" for every situation.
In cases with strong domain-specific characteristics, such as Unreal Engine's Actor, when creativity is essential in a GUI framework, or in systems where performance is critical, it is more important to choose a design that fits the context rather than forcing SOLID compliance. Recognizing that boundary, of course, comes down to a programmer's experience.
The C++ programmer John F. Woods said in 1991,
"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live" is not simply a warning to write readable code.
The deeper meaning behind it is to make sure that whoever works with the code can understand and adapt to it easily, regardless of the circumstances.
SOLID is one path toward that goal, not the only one. Sometimes the pragmatic choice that keeps that "psychopath" from going on a rampage is to break SRP and maintain a consolidated class, or to ignore DIP and opt for a concrete dependency instead.