Lingua-e
← Back

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

  1. 1. S: Single Responsibility Principle
  2. 2. O: Open/Closed Principle
  3. 3. L: Liskov Substitution Principle
  4. 4. I: Interface Segregation Principle
  5. 5. D: Dependency Inversion Principle
  6. 6. How to use SOLID terms in code reviews

Vocabulary table

Key terms you will need when talking about SOLID in English:

TermSpanishExample sentence
Single ResponsibilityResponsabilidad únicaThis class has too many responsibilities. It should only handle user authentication.
Open/ClosedAbierto/CerradoWe should extend this, not modify it, to follow the Open/Closed Principle.
Liskov SubstitutionSustitución de LiskovA subclass should be usable anywhere the parent class is expected.
Interface SegregationSegregación de interfacesClients should not be forced to depend on methods they do not use.
Dependency InversionInversión de dependenciasHigh-level modules should not depend on low-level modules.
tight couplingacoplamiento fuerteThis design is tightly coupled, which makes it hard to test.
abstractionabstracciónWe need an abstraction layer between the service and the database.
inherit / inheritanceheredar / herenciaThis 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.

Not SOLID: one class doing too much
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.

SOLID: one class, one responsibility
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.

Not SOLID: adding a new type requires editing existing code
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 price

Problem: 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.

SOLID: extend without modifying
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.

Not SOLID: subclass breaks the parent contract
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 Penguin

Problem: 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.

SOLID: subclasses honor the parent 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 surprises

Now 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.

Not SOLID: one big interface forces unnecessary methods
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.

SOLID: small, focused interfaces
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 needs

Now 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.

Not SOLID: high-level class depends on 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.

SOLID: depend on abstractions, not concrete classes
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
Roxana Lafuente

Written by

Roxana Lafuente

Lingua-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.