Top 50 SOLID Principles Interview Questions & Answers (2026)
About SOLID Principles
Top 50 SOLID Principles interview questions covering SRP, OCP, LSP, ISP, DIP, and their application in software design. Companies hiring for SOLID Principles roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.
What to Expect in a SOLID Principles Interview
Expect a mix of conceptual and practical SOLID Principles questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.
How to Use This Guide
Work through the SOLID Principles questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.
Curated by Tech Baithak Editorial Team · Last updated: June 2026
Core concepts every SOLID Principles developer must know.
01
What are the SOLID principles?
SOLID is an acronym for five foundational object-oriented design principles introduced by Robert C. Martin (Uncle Bob). They are: S — Single Responsibility Principle (a class should have only one reason to change), O — Open/Closed Principle (open for extension, closed for modification), L — Liskov Substitution Principle (subtypes must be substitutable for their base types), I — Interface Segregation Principle (clients should not depend on interfaces they don't use), and D — Dependency Inversion Principle (depend on abstractions, not concretions). Together, these principles guide developers toward code that is maintainable, testable, and extensible over time.
02
What does the Single Responsibility Principle (SRP) mean?
The Single Responsibility Principle states that a class should have only one reason to change, meaning it should have only one job or responsibility. A class that handles user authentication, sends emails, and writes audit logs has three reasons to change — violating SRP. Splitting it into AuthService, EmailService, and AuditLogger gives each class a single, well-defined purpose. SRP makes code easier to understand, test, and maintain. When requirements change for one concern (e.g., switching from SMTP to SendGrid), only the relevant class needs modification, reducing the risk of breaking unrelated functionality.
03
What does the Open/Closed Principle (OCP) mean?
The Open/Closed Principle states that software entities (classes, modules, functions) should be open for extension but closed for modification. This means you should be able to add new behavior without changing existing, tested code. For example, if you have a PaymentProcessor that handles credit cards and you need to add PayPal support, OCP says you should extend the system (by adding a new PayPalPayment class implementing a Payment interface) rather than modifying the existing PaymentProcessor. This reduces the risk of introducing bugs in working code. OCP is typically achieved through polymorphism, interfaces, and design patterns like Strategy, Decorator, and Factory.
04
What does the Liskov Substitution Principle (LSP) mean?
The Liskov Substitution Principle, formulated by Barbara Liskov, states that objects of a subclass must be replaceable by objects of the parent class without breaking the program's correctness. If you have a function that accepts a Bird object and calls bird.fly(), and you pass a Penguin subclass that throws an exception on fly(), you violate LSP. A classic example: a Square that extends Rectangle might violate LSP because setting width on a Square also changes height, breaking the rectangle's behavior contract. LSP ensures that inheritance hierarchies are behaviorally consistent, not just structurally similar.
05
What does the Interface Segregation Principle (ISP) mean?
The Interface Segregation Principle states that clients should not be forced to depend on methods they do not use. Instead of one large, general-purpose interface, you should create smaller, role-specific interfaces. For example, a Worker interface with methods work(), eat(), and sleep() forces a RobotWorker to implement eat() and sleep() which are irrelevant to it. Better design: separate Workable, Eatable, and Sleepable interfaces. ISP keeps interfaces cohesive and prevents classes from being burdened with methods they don't need, reducing coupling and making implementations more focused.
06
What does the Dependency Inversion Principle (DIP) mean?
The Dependency Inversion Principle has two rules: (1) High-level modules should not depend on low-level modules — both should depend on abstractions. (2) Abstractions should not depend on details — details should depend on abstractions. Without DIP, a ReportService (high-level) might directly instantiate a MySQLDatabase (low-level), creating tight coupling. With DIP, ReportService depends on a DatabaseInterface abstraction, and MySQLDatabase implements that interface. You can swap in PostgreSQLDatabase or MockDatabase without changing ReportService. DIP is the foundation of dependency injection and is critical for testability.
07
Why are SOLID principles important in software development?
SOLID principles are important because they address the fundamental challenges of growing software: rigidity (hard to change), fragility (breaks in unexpected places), and immobility (hard to reuse components). Following SOLID leads to code that is easier to test (isolated responsibilities can be unit-tested), easier to maintain (changes are localized), easier to extend (new features don't break existing code), and easier to understand (each class has a clear purpose). While they require upfront design effort, they pay dividends in long-lived projects where requirements evolve. They are especially critical in team environments where multiple developers work on the same codebase.
08
What is a "code smell" and how does it relate to SOLID?
A code smell is a surface indication in the code that something may be wrong with the underlying design — it doesn't necessarily mean there's a bug, but it suggests a potential problem worth investigating. Code smells often indicate SOLID violations: a God Class (class that does everything) violates SRP; a class that requires modification every time a new feature is added violates OCP; an interface with many methods that most implementors leave empty violates ISP; classes that directly instantiate their dependencies violate DIP. Common code smells include long methods, large classes, duplicate code, long parameter lists, and inappropriate intimacy. Recognizing these smells is the first step toward applying SOLID principles as a refactoring guide.
09
Give an example of SRP violation.
A classic SRP violation is a User class that handles authentication, profile management, email sending, and database persistence all in one. Consider: class User { login() {}, logout() {}, updateProfile() {}, sendWelcomeEmail() {}, saveToDatabase() {} }. This class has five distinct reasons to change: changes to auth logic, profile structure, email templates, or database schema all require modifying the same class. A SOLID refactoring splits this into AuthService, UserProfileService, EmailService, and UserRepository. Each class now has a single, clearly defined responsibility and can be modified, tested, or replaced independently.
10
How does OCP promote extensibility?
OCP promotes extensibility by encouraging a design where new behavior is added through new code, not by modifying existing code. This is achieved by identifying the variation points in a system and abstracting them behind interfaces or abstract classes. For example, a logging system that supports FileLogger and needs to add DatabaseLogger follows OCP if both implement a Logger interface — the calling code doesn't change. Systems designed with OCP in mind tend to use plugins, strategies, and hooks to allow extension points. The payoff is that the risk of regression is dramatically reduced: existing, tested code is never touched when new behavior is added.
11
What is the difference between SOLID principles and design patterns?
SOLID principles are high-level guidelines for software design — they describe what properties good code should have (single responsibility, extensibility, substitutability, etc.) without prescribing specific implementation approaches. Design patterns are concrete, reusable solutions to commonly occurring design problems — they are implementation blueprints (e.g., Factory, Observer, Strategy). The two are complementary: design patterns are often the mechanism for achieving SOLID. For instance, the Strategy pattern helps implement OCP, the Decorator pattern helps implement OCP and SRP, and the Dependency Injection pattern implements DIP. SOLID tells you what to aim for; design patterns give you how to get there.
12
What is tight coupling and how does it relate to SOLID?
Tight coupling occurs when a class directly depends on the concrete implementation of another class — knowing its internal details and being unable to function without that specific dependency. Example: class OrderService { private db = new MySQLDatabase(); } — OrderService is tightly coupled to MySQLDatabase. Tight coupling violates multiple SOLID principles: it violates DIP (depending on concretion, not abstraction), makes testing harder (can't mock the database), and makes the code fragile (changing the database requires changing OrderService). SOLID principles, especially DIP and ISP, directly combat tight coupling by promoting interfaces and dependency injection as the preferred way to connect components.
13
What is loose coupling and why is it desirable?
Loose coupling means that a class depends on an abstraction (interface) of another class rather than its concrete implementation. class OrderService { constructor(private db: DatabaseInterface) {} } — OrderService only knows the DatabaseInterface contract, not the specific implementation. Loose coupling is desirable because it enables: (1) Testability — you can inject a mock database in tests; (2) Flexibility — you can swap implementations without modifying the consumer; (3) Parallel development — teams can work on different implementations simultaneously; (4) Reusability — loosely coupled modules can be used in different contexts. DIP is the SOLID principle most directly responsible for promoting loose coupling.
14
How does SRP help with testing?
SRP dramatically improves testability by ensuring that each class has a small, focused surface area to test. A class with a single responsibility has fewer states to account for, fewer dependencies to mock, and a simpler test setup. Compare testing a 500-line God Class (complex setup, many dependencies, unclear test boundaries) to testing a 50-line PriceCalculator class (takes inputs, returns a price, pure logic, no mocks needed). When a class follows SRP, its unit tests are precise — a failing test clearly indicates which responsibility is broken. Additionally, SRP-compliant classes tend to be easier to make deterministic (no side effects mixed with pure logic), which is a prerequisite for reliable automated tests.
15
What is cohesion in software design?
Cohesion measures how strongly related and focused the responsibilities within a module or class are. A highly cohesive class has all its methods and properties working toward a single, well-defined purpose — like a ShoppingCart class that only manages cart items, quantities, and totals. A low cohesion class has unrelated methods lumped together — like a Utilities class with date formatting, user validation, and file compression. High cohesion is a direct outcome of following SRP. Cohesion is the counterpart to coupling: good design aims for high cohesion and low coupling simultaneously. Cohesion is measured within a module; coupling measures relationships between modules.
16
How does ISP differ from SRP?
While both principles deal with responsibility and focus, they operate at different levels. SRP applies to classes — it says a class should have one reason to change, one responsibility. ISP applies to interfaces — it says interfaces should be narrow and role-specific so that implementing classes aren't forced to provide methods they don't need. You can have a class that follows SRP (single responsibility) but still violates ISP by implementing a fat interface that includes methods irrelevant to its role. Conversely, a class might implement only small, segregated interfaces but still have multiple internal responsibilities. SRP is about class design; ISP is about interface design. Together, they enforce focused, minimal contracts throughout the design.
17
What is dependency injection?
Dependency Injection (DI) is a technique where a class receives its dependencies from an external source rather than creating them internally. Instead of class Service { private db = new Database(); }, the dependency is injected: class Service { constructor(private db: DatabaseInterface) {} }. There are three forms: Constructor injection (most common and recommended), setter injection (via setter methods), and interface injection. DI is the practical implementation of DIP — by injecting abstractions, you decouple the class from concrete implementations. DI containers (Spring in Java, Laravel's service container in PHP, NestJS IoC in Node.js) automate the wiring of dependencies across large applications.
18
How does DIP relate to dependency injection?
DIP is the principle; Dependency Injection is the pattern that implements it. DIP states that high-level modules should depend on abstractions, not concretions. Dependency Injection is the mechanism that makes this possible: instead of a class creating its own dependencies (new ConcreteService()), they are provided (injected) from outside. Without DI, achieving DIP requires manually passing dependencies through constructors or factory methods. DI containers automate this by maintaining a registry of interface-to-implementation mappings and resolving the full dependency graph automatically. Laravel's service container, for example, automatically injects a concrete EmailService wherever an EmailInterface is type-hinted, fully implementing DIP with minimal boilerplate.
19
What is the role of interfaces/abstractions in SOLID?
Interfaces and abstractions are the connective tissue that makes most SOLID principles work in practice. They appear in: OCP — you extend behavior by adding new implementations of an interface, not modifying existing classes; LSP — the interface defines the behavioral contract that all implementations must honor; ISP — well-designed interfaces are narrow and role-specific; DIP — both high-level and low-level modules depend on interfaces rather than each other. Without interfaces, OCP and DIP are nearly impossible to implement — you'd always be modifying concrete classes. Interfaces also enable polymorphism, allowing client code to work with any implementation that satisfies the contract.
20
What is a "god class" and which SOLID principle does it violate?
A God Class (also called a Blob anti-pattern) is a class that knows too much or does too much — it accumulates an excessive number of responsibilities over time, becoming the central hub of an application. Symptoms include hundreds or thousands of lines of code, dozens of methods spanning unrelated concerns (authentication, business logic, data access, formatting), and every other class depending on it. A God Class primarily violates SRP — it has many reasons to change. It also tends to violate OCP (modification is needed for any new feature), DIP (it's often tightly coupled to many concrete classes), and ISP (it often forces clients to depend on a large, bloated interface). Refactoring a God Class involves identifying distinct responsibilities and extracting them into focused classes.
Practical knowledge for developers with hands-on experience.
01
How would you refactor a class that violates SRP?
The process starts with identifying distinct responsibilities within the class by asking "what reasons does this class have to change?" For each distinct reason, extract the related methods and properties into a new, focused class. Then inject the new classes into the original via constructor injection. Example: a UserService that validates, saves, and emails users gets split into UserValidator, UserRepository, and UserEmailService. The original UserService becomes an orchestrator that coordinates these single-responsibility services. This refactoring should be done incrementally with test coverage at each step. Use IDE refactoring tools (Extract Class, Move Method) to reduce manual errors during the process.
02
How does the Strategy pattern support OCP?
The Strategy pattern is one of the most direct implementations of OCP. It defines a family of algorithms, encapsulates each one in a class, and makes them interchangeable. The context class holds a reference to a strategy interface and delegates behavior to whichever implementation is injected. When a new algorithm is needed, you add a new strategy class without modifying the context. Example: a Sorter class that takes a SortStrategy interface can use BubbleSortStrategy, QuickSortStrategy, or any future strategy — the Sorter class itself never changes. This is OCP in action: the context is closed for modification but open for extension via new strategies. Spring's Comparator, Laravel's drivers, and payment gateways commonly follow this pattern.
03
What are the conditions for LSP compliance?
A subclass is LSP-compliant with its parent when it satisfies these behavioral contracts: (1) Preconditions cannot be strengthened — if the parent accepts any integer, the subclass cannot restrict to positive integers only; (2) Postconditions cannot be weakened — if the parent guarantees a non-null return, the subclass cannot return null; (3) Invariants must be preserved — properties that are always true of the parent must remain true in the subclass; (4) No new exceptions — the subclass cannot throw exceptions that the parent doesn't throw (or subtypes of them); (5) History constraint — if the parent's state only changes in certain ways, the subclass must respect those constraints. These conditions together ensure that code using the parent type works correctly with any subtype.
04
How does ISP prevent "fat interfaces"?
ISP prevents fat interfaces by encouraging role-based interface design — instead of designing an interface from the perspective of a single implementor, you design it from the perspective of each client. Each client only sees the methods it actually needs. For example, a Printable interface with print(), scan(), fax(), and copy() is fat — a simple printer only prints but must implement all methods. ISP splits this into Printable, Scannable, Faxable, and Copyable interfaces. Implementors compose only what they support. The practical benefit: adding a new method to a fat interface breaks all implementors; adding a method to a small interface only affects those that choose to implement it. ISP-driven interfaces tend to be more stable and versioned independently.
05
What is the difference between DIP and dependency injection containers?
DIP is the design principle (high-level modules depend on abstractions), while Dependency Injection is the pattern to achieve it, and a DI Container (IoC Container) is a framework tool that automates the injection. You can practice DIP and DI without a container by manually passing dependencies through constructors. Containers add automatic dependency resolution: you register bindings (bind(DatabaseInterface::class, MySQLDatabase::class)), and the container resolves the full dependency graph automatically when you resolve a service. Laravel's service container, Spring's ApplicationContext, and NestJS's IoC module are examples. Containers also manage lifecycle (singleton vs. transient instances) and handle circular dependency detection.
06
How does SOLID relate to GRASP principles?
GRASP (General Responsibility Assignment Software Patterns) is a set of nine principles for assigning responsibilities to classes, formulated by Craig Larman. SOLID and GRASP overlap significantly but approach design from different angles. GRASP's Information Expert (assign responsibility to the class that has the necessary information) parallels SRP. GRASP's Low Coupling and High Cohesion are the underlying goals that SOLID principles achieve through specific rules. GRASP's Polymorphism principle supports OCP. GRASP's Protected Variations principle (identify points of variation and create stable interfaces around them) is essentially OCP stated differently. SOLID is more prescriptive and widely adopted; GRASP is more analytical and often taught in academic software engineering courses.
07
How do SOLID principles apply to functional programming?
SOLID originated in OOP context but the underlying ideas translate to functional programming (FP). SRP maps to small, focused functions that do one thing. OCP maps to higher-order functions and function composition — extend behavior by composing functions, not modifying existing ones. LSP maps to the concept of parametric polymorphism — a function working on type A should work correctly for any subtype. ISP maps to narrowly typed function signatures — accept only the data fields a function actually uses. DIP maps to passing functions (behaviors) as parameters (higher-order functions) instead of hardcoding algorithms. In FP, immutability and pure functions naturally enforce many SOLID goals by design, making some violations structurally impossible.
08
What are the trade-offs of strictly following SOLID?
Strict SOLID adherence has real costs. (1) Increased number of classes/interfaces — splitting responsibilities creates more files, more navigation overhead, and more cognitive load for newcomers. (2) Over-abstraction — creating interfaces for everything adds indirection that makes code harder to trace and understand (Java enterprise patterns like the infamous "AbstractSingletonProxyFactoryBean" are examples). (3) Premature optimization — designing for extension before understanding what will actually change can lead to the wrong abstractions. (4) Performance overhead — polymorphism and dependency injection add runtime indirection. The pragmatic approach: apply SOLID principles where complexity justifies it. Simple scripts and small functions don't need DIP. Apply SOLID proportionally to the likelihood and cost of change.
09
How does OCP work with inheritance vs composition?
OCP can be implemented through both inheritance and composition, but they have different trade-offs. Inheritance-based OCP: a base class provides default behavior, subclasses override to extend — the classic Template Method pattern. The risk is fragile base class problem — changes to the base class can unintentionally break subclasses. Composition-based OCP (preferred): a class delegates behavior to an interface, and new behavior is added by creating new implementations. This follows the Gang of Four advice: "prefer composition over inheritance." Composition-based OCP is more flexible because implementations can be mixed and matched at runtime, while inheritance is fixed at compile time. The Strategy, Decorator, and Chain of Responsibility patterns all use composition to achieve OCP.
10
What is the Barbara Liskov quotation that defines LSP?
Barbara Liskov's formal definition from her 1988 paper states: "If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T." In simpler terms: a program that works correctly with a Bird object must work correctly when any subtype of Bird is substituted, without knowing the concrete type. This is a stricter definition than mere structural substitutability (the "is-a" relationship) — it demands behavioral substitutability. The principle emphasizes that subtyping is about behavior contracts, not just method signatures, and is what distinguishes LSP from mere type inheritance.
11
How does DIP improve testability?
DIP improves testability by making it possible to substitute real dependencies with test doubles (mocks, stubs, fakes). When a class depends on a concrete implementation (e.g., MySQLDatabase), unit testing it requires a real database — slow, fragile, and stateful. When it depends on an interface (DatabaseInterface), tests can inject a MockDatabase that returns controlled data without network calls. This makes tests fast (no I/O), isolated (no shared state), deterministic (predictable responses), and focused (testing only the class's logic). DIP combined with constructor injection enables new OrderService(new MockDatabase()) in test setup. This is why frameworks like Laravel use DIP extensively — the service container makes both production and test wiring seamless.
12
What is "design by contract" and how does it relate to LSP?
Design by Contract (DbC), introduced by Bertrand Meyer, is a software design approach where components define formal agreements: preconditions (what the caller must guarantee before calling), postconditions (what the callee guarantees after returning), and invariants (properties always true of the object). LSP is directly expressed in DbC terms: a subclass must accept at least as weak preconditions as the parent (can be more permissive but not stricter) and must provide at least as strong postconditions (can guarantee more but not less). If the parent guarantees a sorted list is returned, the subclass must also return a sorted list. DbC makes LSP violations detectable at the specification level before any code is written. Languages like Eiffel have built-in DbC support; others use assertions and runtime checks.
13
How do SOLID principles apply to microservices?
SOLID scales from class design to service design in microservices. SRP at service level: each microservice should have one bounded context — an order service handles orders, a user service handles users. Services with multiple domains become distributed monoliths. OCP: services should be extensible via new endpoints or event consumers without breaking existing contracts. LSP: in API versioning, v2 of an API should remain substitutable for v1 — clients using v1 contracts should not break when upgraded. ISP: expose only the API surface that clients need; use API gateway patterns to create role-specific facades. DIP: services communicate via abstract contracts (OpenAPI specs, message schemas) rather than direct code dependencies, enabling independent deployment.
14
What is cohesion vs coupling and how do SOLID principles address both?
Cohesion measures how related a module's internal elements are to each other; coupling measures how dependent one module is on another. The ideal is high cohesion, low coupling. SOLID addresses cohesion through SRP — a class with one responsibility has high cohesion by definition — and ISP — interfaces that reflect a single role are highly cohesive. SOLID addresses coupling through DIP — depending on abstractions instead of concretions reduces coupling between modules — and OCP — extending via new classes rather than modifying existing ones avoids increasing coupling. LSP enables polymorphism safely, allowing high-level modules to remain decoupled from specific implementations while trusting the behavioral contract.
15
How do you apply SOLID in a legacy codebase?
Applying SOLID to a legacy codebase is an incremental, risk-aware process called the Boy Scout Rule approach (leave the code a little better than you found it). The strategy: (1) Write tests first — before refactoring, ensure characterization tests that capture current behavior; (2) Extract methods — break long methods into smaller, named functions (SRP at method level); (3) Extract classes — identify distinct responsibilities in God Classes and extract them; (4) Introduce interfaces — wrap external dependencies (DB, email) in interfaces and inject them (DIP); (5) Use the Strangler Fig pattern — gradually replace old code paths with new, SOLID-compliant code rather than big-bang rewrites. Never apply SOLID to untested legacy code recklessly; each refactoring step must be backed by tests.
16
How does the Decorator pattern support OCP?
The Decorator pattern is a textbook implementation of OCP. It adds behavior to an object dynamically without modifying its class. Both the decorator and the decorated object implement the same interface. Example: a Logger interface implemented by ConsoleLogger. To add timestamping, create TimestampLogger(Logger wrapped) that adds a timestamp before delegating to the wrapped logger. To add encryption, create EncryptedLogger(Logger wrapped). You can compose these at runtime: new EncryptedLogger(new TimestampLogger(new ConsoleLogger())). Neither the original ConsoleLogger nor any previous decorator is modified when new behavior is added. Laravel's middleware pipeline, Java I/O streams, and many plugin systems use the Decorator pattern.
17
What is "interface pollution" and which principle prevents it?
Interface pollution occurs when a single interface accumulates too many methods, forcing implementors to provide methods that are irrelevant to their actual role. This happens when interfaces grow over time without discipline, often because developers keep adding methods to a convenient existing interface. For example, an Animal interface that starts with eat() and breathe() and gradually gains fly(), swim(), and burrow() becomes polluted — most animals implement only a subset of these behaviors but are forced to provide empty or exception-throwing implementations for the rest. ISP directly prevents interface pollution by mandating that interfaces be client-specific. The solution is to split the fat interface into Flyable, Swimmable, and Burrowable interfaces that animals selectively implement.
18
How do SOLID principles relate to YAGNI and DRY?
YAGNI (You Ain't Gonna Need It) and DRY (Don't Repeat Yourself) are complementary principles that must be balanced with SOLID. Tension exists: SOLID (especially OCP) encourages designing extension points even before you need them, which can conflict with YAGNI (only build what's needed now). The resolution is to apply SOLID reactively: when you see a second concrete variation or a second reason to change, that's the signal to introduce an abstraction (the Rule of Three). DRY eliminates duplication, but over-applying it can lead to premature abstractions that violate SRP by combining things that change for different reasons. Martin Fowler's advice: follow DRY when duplication represents the same knowledge; allow duplication when it represents different knowledge that happens to look the same currently.
19
How does SRP apply at the module/package level?
SRP scales beyond classes to modules, packages, and services. At the package level, SRP manifests as the Common Closure Principle (CCP): classes that change for the same reason should be in the same package; classes that change for different reasons should be in different packages. A package that mixes domain logic, infrastructure code, and presentation logic has multiple reasons to change — violating package-level SRP. In practice, this means organizing packages by feature/domain (vertical slicing) rather than by technical layer (horizontal slicing). A users package contains the user model, repository, service, and controller — all things that change when the user feature changes. This reduces the need to modify multiple packages for a single feature change.
20
What are common misconceptions about OCP?
Several common misconceptions surround OCP: (1) "Never modify code" — OCP doesn't mean code can never change; it means that once a module is stable and tested, behavior should be extended without modifying that module. Bug fixes and initial development are exceptions. (2) "Use inheritance for extension" — inheritance is just one mechanism; composition and interfaces are often superior. (3) "Anticipate all variations" — you can't predict every future extension point. OCP should be applied where change has already been observed (after the first variation), not speculatively everywhere. (4) "OCP is about open source" — completely unrelated. (5) "OCP applies to every class" — small utility classes and value objects rarely need OCP treatment. Apply OCP proportionally to volatility: design extension points where change is likely and costly.
Deep expertise questions for senior and lead roles.
01
How do SOLID principles apply to domain-driven design?
SOLID and Domain-Driven Design (DDD) are deeply complementary. DDD's Bounded Contexts enforce SRP at the macro level — each context has a single, well-defined domain responsibility. DDD's Domain Entities and Value Objects benefit from SRP (no persistence, serialization, or presentation logic in domain models) and OCP (domain behavior extended via Specifications, domain events). DDD's Repository pattern is a direct application of DIP — the domain layer depends on a repository interface, and the infrastructure layer provides the implementation. DDD's Domain Services and Application Services follow SRP by separating domain logic from coordination logic. LSP is important in DDD's aggregate hierarchies — subtypes must honor the root aggregate's invariants. ISP manifests in DDD's Context Maps — each bounded context only exposes the interface needed by consuming contexts.
02
How do you balance SOLID principles with pragmatic software development?
Balancing SOLID with pragmatism requires knowing when to apply each principle. The key heuristic is proportionality to volatility and consequence: apply SOLID rigorously where code is likely to change, is mission-critical, or is shared across teams. Apply it lightly for throwaway scripts, glue code, or areas unlikely to evolve. Pragmatic rules: (1) Wait for the second variation before introducing an abstraction (YAGNI first, then OCP); (2) Don't inject dependencies that have only one implementation and are unlikely to change — the abstraction adds indirection with no benefit; (3) Start with simple, working code and refactor toward SOLID when pain is felt (tests become hard, change causes regressions); (4) In rapidly prototyped features, accept some SOLID debt, then pay it down before the feature goes to production. SOLID is a tool, not a dogma.
03
How does the Hexagonal Architecture (Ports and Adapters) apply DIP?
Hexagonal Architecture (also called Ports and Adapters, coined by Alistair Cockburn) is DIP applied at the architectural level. The domain (core business logic) sits at the center and defines ports — interfaces describing what it needs from the outside world (persistence, messaging, external services). Adapters are concrete implementations of those ports — a MySQL adapter, a SendGrid adapter, a REST adapter. The direction of dependency is always inward: adapters depend on port interfaces; the domain depends on nothing outside itself. This means the domain is fully testable without any infrastructure, can be driven by multiple adapters simultaneously (REST API + CLI + message queue), and infrastructure can be swapped without changing domain logic. Laravel's repository pattern partially implements this; clean architecture frameworks go further.
04
What is the relationship between SOLID and Clean Architecture?
Clean Architecture (Robert C. Martin) is the macro-level architectural pattern that embodies all five SOLID principles simultaneously. Its concentric ring structure enforces: SRP — each ring (Entities, Use Cases, Interface Adapters, Frameworks) has a distinct responsibility; OCP — inner rings are closed to changes from outer rings; LSP — outer rings implement interfaces defined by inner rings, and must be behaviorally substitutable; ISP — boundaries between rings are defined by narrow, role-specific interfaces (input/output ports); DIP — the Dependency Rule (all dependencies point inward) is DIP applied structurally — outer layers depend on inner abstractions, never the reverse. Clean Architecture is essentially SOLID principles applied at the architecture level, creating systems where frameworks, databases, and UIs are implementation details that can be swapped without affecting the core business logic.
05
How does LSP interact with covariance and contravariance in type systems?
LSP has precise implications for how type parameters behave in subtyping relationships, expressed through variance. Covariance (safe for return types): if Cat is a subtype of Animal, a method returning Cat in a subclass is an acceptable override of one returning Animal — the postcondition is strengthened (more specific), not weakened. Contravariance (safe for parameter types): a method accepting Animal as parameter in a subclass is an acceptable override of one accepting Cat — the precondition is weakened (more permissive). Invariance: mutable generic containers (e.g., List<Cat>) cannot be treated as List<Animal> because you could add a Dog to what's actually a list of cats. TypeScript's structural typing with strict function parameter checking, Kotlin's in/out keywords, and Java's wildcards (? extends T, ? super T) are all implementations of variance rules derived from LSP requirements.
06
How do SOLID principles apply to event-driven architectures?
SOLID principles remain highly relevant in event-driven architectures (EDA). SRP: each event handler has one responsibility — process one event type. Avoid handlers that react to many unrelated events. OCP: adding new behavior to a system by publishing new event types or adding new subscribers, not modifying existing handlers. The event bus itself is closed for modification but open for extension via new subscriber registrations. LSP: events in the same hierarchy (e.g., OrderPlaced, OrderCancelled both extending OrderEvent) must be handleable by code written against the base type without special-casing. ISP: subscribers subscribe to specific event types, not a fat interface of all possible events. DIP: producers and consumers are decoupled through the event bus abstraction — neither knows about the other. Saga patterns and process managers in DDD apply SRP within complex event choreography.
07
What metrics can you use to measure SOLID compliance?
Several code metrics correlate with SOLID compliance: (1) LCOM (Lack of Cohesion in Methods) — measures SRP compliance; high LCOM indicates methods that don't share instance variables, suggesting a class has multiple responsibilities. (2) Afferent/Efferent Coupling (Ca/Ce) and Instability — measures DIP compliance; core modules should be stable (high Ca, low Ce); frameworks should be instable (high Ce). (3) Abstractness — ratio of abstract types to all types; combined with instability gives the Distance from Main Sequence metric. (4) Number of Methods per Interface — high counts suggest ISP violations. (5) Number of Direct Concrete Dependencies — high counts suggest DIP violations. Tools like JDepend (Java), NDepend (.NET), PHPMetrics (PHP), and SonarQube measure these. But metrics are proxies — human code review remains essential for true SOLID assessment.
08
How do SOLID principles evolve in a growing codebase?
SOLID compliance tends to degrade over time through entropy unless deliberately maintained. Common patterns: SRP violations accumulate through "just add it here" feature additions; OCP violations emerge when developers patch existing classes rather than extending; ISP violations grow as interfaces absorb new methods for edge cases. Managing SOLID in a growing codebase requires: (1) Architecture fitness functions — automated checks that verify dependency direction rules (ArchUnit in Java, Deptrac in PHP); (2) Regular design reviews — periodic examination of growing classes and interfaces; (3) Definition of Ready in Agile — require design review before implementing new features in critical modules; (4) Code ownership — teams own SOLID compliance for their bounded context; (5) Refactoring sprints — allocate time specifically for SOLID improvements when technical debt becomes painful. Continuous attention to metrics (LCOM, coupling) helps detect violations early.
09
How do SOLID principles differ in dynamically-typed vs statically-typed languages?
SOLID principles are language-agnostic in intent but their implementation differs significantly based on type system. In statically-typed languages (Java, C#, TypeScript), SOLID is enforced at compile time: interfaces are explicit types, DIP is implemented via type annotations, and LSP violations can be caught by the type checker. In dynamically-typed languages (Python, Ruby, JavaScript), there are no compile-time interface checks. DIP is implemented via duck typing — any object with the right methods works. LSP violations only manifest at runtime. OCP is achieved through monkey patching (risky) or proper extension classes. ISP is less formally enforced — you rely on convention and documentation. The SOLID intent (single responsibility, extensibility, substitutability) is still valuable, but the enforcement mechanisms are testing, code review, and conventions like Python's Protocol type hints, rather than compiler checks. Dynamically-typed languages often achieve DIP through framework conventions (Ruby on Rails, Django) rather than explicit DI containers.
10
How do you teach SOLID principles to a development team?
Teaching SOLID effectively requires moving beyond definitions to recognition and practice. Effective approaches: (1) Code smell → principle mapping — start with identifying smells in the team's own codebase (God Classes, long methods, hard-to-test code) and connect each to the SOLID principle it violates. Learning from real pain is more memorable than abstract examples. (2) Before/after refactoring exercises — show a violating codebase, have developers refactor it, then discuss trade-offs. (3) Coding katas — exercises like Bowling Game, Roman Numerals, or Gilded Rose Refactoring Kata build SOLID muscle memory. (4) Pair programming and code review — real-time SOLID guidance during peer review normalizes the vocabulary and patterns. (5) Gradual introduction — start with SRP (most intuitive), then OCP, and build to DIP (most abstract). (6) Anti-pattern recognition — teams learn faster by seeing what NOT to do. Document SOLID decisions in architecture decision records (ADRs) to create institutional memory.