Design principles in English
SOLID Principles Explained in English: A Guide for Developers
What each letter stands for, what it means in plain English, and Python code examples showing the difference between SOLID and non-SOLID code.
Why SOLID comes up so often in English
If you work on an international team or read English documentation, you will hear SOLID a lot. In pull requests: "This violates the Single Responsibility Principle." In code reviews: "We should follow the Open/Closed Principle here." In technical interviews: "Can you explain SOLID to me?"
SOLID is an acronym. Each letter stands for a design principle. These principles were introduced by Robert C. Martin (also known as Uncle Bob) and are widely used in object-oriented programming. Knowing the terms in English is as important as understanding the concepts themselves.
In this article
Vocabulary table
Key terms you will need when talking about SOLID in English:
| Term | Spanish | Example sentence |
|---|---|---|
| Single Responsibility | Responsabilidad única | This class has too many responsibilities. It should only handle user authentication. |
| Open/Closed | Abierto/Cerrado | We should extend this, not modify it, to follow the Open/Closed Principle. |
| Liskov Substitution | Sustitución de Liskov | A subclass should be usable anywhere the parent class is expected. |
| Interface Segregation | Segregación de interfaces | Clients should not be forced to depend on methods they do not use. |
| Dependency Inversion | Inversión de dependencias | High-level modules should not depend on low-level modules. |
| tight coupling | acoplamiento fuerte | This design is tightly coupled, which makes it hard to test. |
| abstraction | abstracción | We need an abstraction layer between the service and the database. |
| inherit / inheritance | heredar / herencia | This class inherits from the base class but overrides its behavior incorrectly. |
S: Single Responsibility Principle
S: Principio de Responsabilidad Única
A class should have only one reason to change. In plain English: each class or function should do one thing and do it well. When you put too many responsibilities in one place, any change to one responsibility can break another.
class UserManager:
def create_user(self, username: str, email: str) -> dict:
# Creates the user in the database
user = {"username": username, "email": email}
print(f"Saving user {username} to DB...") # database logic
return user
def send_welcome_email(self, email: str) -> None:
# Also handles email sending — this is a second responsibility
print(f"Sending welcome email to {email}...")
def generate_report(self) -> str:
# And report generation — a third responsibility
return "User report: ..."Problem: UserManager handles user creation, email sending, and report generation. These are three different reasons to change. If the email provider changes, you need to edit this class even though user creation has nothing to do with emails.
class UserRepository:
def create_user(self, username: str, email: str) -> dict:
user = {"username": username, "email": email}
print(f"Saving user {username} to DB...")
return user
class EmailService:
def send_welcome_email(self, email: str) -> None:
print(f"Sending welcome email to {email}...")
class UserReportService:
def generate_report(self) -> str:
return "User report: ..."Now each class has a single responsibility. Changing the email provider only touches EmailService. Changing the report format only touches UserReportService.
O: Open/Closed Principle
O: Principio Abierto/Cerrado
Software entities should be open for extension, but closed for modification. In plain English: you should be able to add new behavior without editing existing code. You extend it, you do not rewrite it.
class DiscountCalculator:
def calculate(self, order_type: str, price: float) -> float:
if order_type == "student":
return price * 0.8 # 20% off
elif order_type == "employee":
return price * 0.7 # 30% off
# Adding "vip" requires editing this function — not closed for modification
elif order_type == "vip":
return price * 0.6 # 40% off
return priceProblem: every time you add a new discount type, you must edit the calculate method. This makes the class fragile and increases the risk of breaking existing logic.
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def apply(self, price: float) -> float:
pass
class StudentDiscount(DiscountStrategy):
def apply(self, price: float) -> float:
return price * 0.8
class EmployeeDiscount(DiscountStrategy):
def apply(self, price: float) -> float:
return price * 0.7
class VipDiscount(DiscountStrategy):
def apply(self, price: float) -> float:
return price * 0.6
class DiscountCalculator:
def __init__(self, strategy: DiscountStrategy) -> None:
self.strategy = strategy
def calculate(self, price: float) -> float:
return self.strategy.apply(price)Now you can add a new discount type by creating a new class. The DiscountCalculator never changes. It is closed for modification but open for extension.
L: Liskov Substitution Principle
L: Principio de Sustitución de Liskov
Objects of a subclass should be replaceable with objects of the parent class without breaking the program. In plain English: if a function works with a Bird, it should still work with any subclass of Bird without unexpected behavior.
class Bird:
def fly(self) -> str:
return "I am flying!"
class Penguin(Bird):
def fly(self) -> str:
# Penguins cannot fly — this breaks the contract defined by Bird
raise NotImplementedError("Penguins cannot fly")
def make_bird_fly(bird: Bird) -> str:
return bird.fly() # This will crash if called with a PenguinProblem: Penguin is a Bird, but you cannot substitute a Penguin where a Bird is expected. Calling make_bird_fly(Penguin()) raises an error. The subclass breaks the contract.
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def move(self) -> str:
pass
class Sparrow(Bird):
def move(self) -> str:
return "I am flying!"
class Penguin(Bird):
def move(self) -> str:
return "I am swimming!"
def make_bird_move(bird: Bird) -> str:
return bird.move() # Works with any Bird subclass, no surprisesNow Penguin and Sparrow both implement move(), but each in their own way. You can safely substitute any Bird subclass without breaking the program.
I: Interface Segregation Principle
I: Principio de Segregación de Interfaces
Clients should not be forced to depend on interfaces they do not use. In plain English: do not create one giant interface that forces all implementors to define methods they do not need. Split it into smaller, focused interfaces.
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self) -> None:
pass
@abstractmethod
def eat_lunch(self) -> None:
pass
@abstractmethod
def attend_meeting(self) -> None:
pass
class Robot(Worker):
def work(self) -> None:
print("Robot is working")
def eat_lunch(self) -> None:
# Robots do not eat — forced to implement a method that makes no sense
raise NotImplementedError("Robots do not eat")
def attend_meeting(self) -> None:
raise NotImplementedError("Robots do not attend meetings")Problem: Robot is forced to implement eat_lunch and attend_meeting even though those concepts do not apply. The interface is doing too much.
from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self) -> None:
pass
class Feedable(ABC):
@abstractmethod
def eat_lunch(self) -> None:
pass
class Attendable(ABC):
@abstractmethod
def attend_meeting(self) -> None:
pass
class HumanWorker(Workable, Feedable, Attendable):
def work(self) -> None:
print("Human is working")
def eat_lunch(self) -> None:
print("Human is eating lunch")
def attend_meeting(self) -> None:
print("Human is attending the meeting")
class Robot(Workable):
def work(self) -> None:
print("Robot is working")
# No eat_lunch, no attend_meeting — only what it actually needsNow Robot only implements Workable. HumanWorker implements all three. Each class only depends on the interfaces it actually uses.
D: Dependency Inversion Principle
D: Principio de Inversión de Dependencias
High-level modules should not depend on low-level modules. Both should depend on abstractions. In plain English: your business logic should not care which specific database or email service you are using. It should talk to an interface, not a concrete implementation.
class MySQLDatabase:
def save(self, data: dict) -> None:
print(f"Saving {data} to MySQL...")
class UserService:
def __init__(self) -> None:
# Hard-coded dependency on MySQL — cannot switch without editing this class
self.db = MySQLDatabase()
def create_user(self, username: str) -> None:
self.db.save({"username": username})Problem: UserService is tightly coupled to MySQLDatabase. If you want to switch to PostgreSQL or use a mock in tests, you have to edit UserService. The high-level module depends on the low-level one.
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def save(self, data: dict) -> None:
pass
class MySQLDatabase(Database):
def save(self, data: dict) -> None:
print(f"Saving {data} to MySQL...")
class PostgreSQLDatabase(Database):
def save(self, data: dict) -> None:
print(f"Saving {data} to PostgreSQL...")
class UserService:
def __init__(self, db: Database) -> None:
# Depends on the abstraction, not the concrete class
self.db = db
def create_user(self, username: str) -> None:
self.db.save({"username": username})
# Usage: inject whichever implementation you want
service = UserService(db=MySQLDatabase())
test_service = UserService(db=PostgreSQLDatabase())Now UserService depends on the Database abstraction. You can inject any database that implements that interface, including a mock in tests. The business logic stays unchanged.
How to use SOLID terms in code reviews and PRs
Cómo usar los términos SOLID en code reviews y PRs
Once you know the vocabulary, you can use it precisely in English code reviews. Here are the most common patterns:
Pointing out a Single Responsibility violation
- "This class is doing too much. Could we split the email logic into a separate service?"
- "This violates the Single Responsibility Principle. The data access and business logic should be separated."
- "Nit: this function has two responsibilities. It fetches the data and formats it. Worth splitting?"
Suggesting the Open/Closed Principle
- "Instead of adding another if/elif here, we could use a strategy pattern to stay open for extension."
- "If we follow Open/Closed here, we would not need to touch this class every time we add a new type."
- "Consider extending this with a subclass rather than modifying the base class directly."
Flagging a Liskov Substitution violation
- "This subclass raises an exception where the parent class returns a value. That breaks Liskov Substitution."
- "The parent class contract says this method always returns a list. The subclass returns None in some cases."
- "We should be able to use this child class anywhere the parent is expected, but right now we cannot."
Pointing out an Interface Segregation issue
- "This interface is too wide. The Robot class is forced to implement methods it will never use."
- "Could we split this into two smaller interfaces so clients only depend on what they need?"
- "Following Interface Segregation here would mean Robot only needs to implement work()."
Suggesting Dependency Inversion
- "This service is tightly coupled to the MySQL implementation. Could we inject the database as a dependency?"
- "If we depend on the abstraction here instead of the concrete class, this becomes much easier to test."
- "This is a good candidate for dependency injection. The high-level module should not care which database we use."
Keep learning
Ready to practice your English at work?
Lingua-e has interactive exercises built around real developer conversations: standups, code reviews, retrospectives, and more. Practice until it comes naturally.
Try Lingua-e for free
Written by
Roxana LafuenteLingua-e's founder
Roxana Lafuente is a software engineer with 8+ years of experience. At the beginning of her career, even though she had already passed the First Certificate in English, she still froze every time she had to speak up in the daily standup. That was a gap nobody was fixing. After 2,000+ standups, she figured out what actually builds fluency: practice that looks like your real work. She built Lingua-e so other developers wouldn't have to take the long road to feel confident working in an international development environment.