SOLID Principles Interview Questions and Answers (2026)
SOLID interview questions usually want a before/after example, not just a definition — can you show a violation and the concrete fix? Here are the SOLID questions that come up most often, each with a real before/after.
-
What is the Single Responsibility Principle, with a before/after example?
A class should have one reason to change. Before: an
Employeeclass that calculates pay, saves itself to the database, and formats a report — three unrelated reasons to modify it (payroll rules change, DB schema changes, report format changes). After: split intoEmployee(data),PayCalculator(pay logic),EmployeeRepository(persistence), andEmployeeReportFormatter(presentation). Each class now changes for exactly one kind of reason, and each can be tested and modified independently. SRP isn't "one method per class" — it's about cohesion: a class should represent one *axis of change*. The practical benefit is that a bug fix to the report format can't accidentally break payroll calculations because they're no longer entangled in the same file. -
What are common SRP violations you see in real production code?
The most common is the "God Service" or "Manager" class that accumulates unrelated responsibilities over time — an
OrderServicethat starts with order creation and slowly absorbs email notifications, inventory updates, logging, and payment logic because "it's already there." Another common one: controllers/API endpoints that contain validation, business logic, and data access directly instead of delegating. A third: domain entities with database or serialization concerns baked in (e.g., an entity class importingSystem.Data.SqlClient). These violations creep in gradually — nobody designs a God class on day one, it accretes one "just add this here" commit at a time. The fix is usually extracting a new collaborator class and injecting it, not a big rewrite, which is why catching SRP drift early in code review matters more than fixing it later. -
What is the Open/Closed Principle, with a before/after example?
Software entities should be open for extension but closed for modification. Before: a
DiscountCalculatorwithif (customerType == "Gold") ... else if (customerType == "Silver") ...— adding a new customer type means editing this method and risking existing logic. After:interface IDiscountPolicy { decimal Apply(decimal price); } class GoldDiscount : IDiscountPolicy { ... } class PlatinumDiscount : IDiscountPolicy { ... } // added, nothing else touchedNow
DiscountCalculatordepends onIDiscountPolicyand adding Platinum tier means adding a new class, not modifying tested code. OCP isn't "never change code" — it's about isolating the *volatile* part (which discount rule applies) from the *stable* part (how discounts get applied), so change is additive rather than invasive. -
How does the Open/Closed Principle relate to the Strategy pattern?
Strategy is the primary mechanical tool for satisfying OCP when the "thing that varies" is an algorithm or behavior. OCP says "don't modify existing code to add new behavior"; Strategy delivers that by putting each behavior behind a shared interface so new behavior is a new implementing class, and the code that uses the strategy (the "context") never changes. The
IDiscountPolicyexample above is literally the Strategy pattern applied to satisfy OCP. This is a useful interview connection to make explicitly: principles like SOLID describe *what* good design looks like, while patterns like Strategy, Decorator, and Observer are *concrete recipes* for achieving it. When someone asks "how do you apply OCP in practice," naming Strategy (or polymorphism generally) as the mechanism shows you understand the relationship, not just the definitions. -
What is the Liskov Substitution Principle, with a before/after example?
Subtypes must be substitutable for their base types without altering the correctness of the program — if code works with a
Bird, it must keep working when handed any subclass ofBird. Before:Birdhas aFly()method;Penguin : Birdoverrides it to throwNotSupportedException. Any code that loops overList<Bird>callingFly()now breaks when aPenguinis in the list, even though it type-checks. After: separate the capability —Birdhas noFly();IFlyingBird : BirdaddsFly(), implemented only by birds that actually fly, whilePenguinjust implementsBird. LSP violations are usually a signal that inheritance was used to model an "is-a" relationship that doesn't actually hold behaviorally, only structurally. -
Walk through the classic Rectangle/Square LSP violation.
Squareseems like a natural subclass ofRectanglemathematically, so a common (bad) design makesSquareoverrideSetWidth/SetHeightto keep both sides equal:class Square : Rectangle { public override void SetWidth(int w) { width = height = w; } public override void SetHeight(int h) { width = height = h; } }Now this code silently breaks:
void Resize(Rectangle r) { r.SetWidth(5); r.SetHeight(10); Assert(r.Area == 50); }Pass in a
Squareand the assertion fails — setting height also changed width, violating an invariant callers reasonably assumed held for anyRectangle. The lesson isn't "never model Square as Rectangle" — it's that inheritance should preserve behavioral contracts (invariants, pre/postconditions), not just structural/mathematical relationships. The fix is usually composition, or making both immutable value types constructed with fixed dimensions instead of mutable setters. -
What is the Interface Segregation Principle, with a before/after example?
Clients shouldn't be forced to depend on methods they don't use. Before: one fat
IWorkerinterface withWork()andEat(); aRobotWorkeris forced to implementEat()with a meaningless throw or empty body because it doesn't eat. After: split intoIWorkable { Work(); }andIFeedable { Eat(); };RobotWorkerimplements onlyIWorkable,HumanWorkerimplements both. Each client depends only on the methods it actually needs. This matters most in codebases with plugin-style or polymorphic collections — a fat interface forces every implementer to carry dead-weight methods, and callers can't tell from the interface alone what a given implementation actually supports, which quietly reintroduces the coupling ISP was meant to remove. -
Why exactly are "fat" interfaces a problem?
A fat interface forces implementers to either genuinely support every method (bloating unrelated implementations) or fake it with
NotImplementedException/no-ops, which is a landmine — callers can't tell from the type system which methods are safe to call. It also creates unnecessary coupling: ifIRepositoryhas 15 methods and a consumer only needsGetById, that consumer's compile-time dependency surface (and what it needs mocked in tests) is 15 methods wide instead of 1. Changing any one of those 15 methods now risks breaking or requiring recompilation of every consumer, even ones that never called it. Segregating into narrow, role-based interfaces (IReadable,IWritable) means each consumer depends on exactly what it uses, tests are simpler to mock, and changes are contained to the interfaces actually affected. -
What is the Dependency Inversion Principle, with a before/after example?
High-level modules shouldn't depend on low-level modules; both should depend on abstractions. Before:
OrderServicedirectly instantiatesSqlOrderRepository— the business logic (high-level) is hard-wired to a specific database technology (low-level), so a database change or unit test requires touchingOrderService. After:class OrderService { private readonly IOrderRepository _repo; // abstraction public OrderService(IOrderRepository repo) => _repo = repo; }Now
OrderServiceandSqlOrderRepositoryboth depend on theIOrderRepositoryabstraction; neither depends on the other directly. This is what makes it possible to swapSqlOrderRepositoryforInMemoryOrderRepositoryin tests, or forMongoOrderRepositorylater, without changingOrderServiceat all. -
What's the actual difference between Dependency Inversion and Dependency Injection?
They're commonly conflated but distinct. Dependency Inversion is a design *principle*: depend on abstractions, not concrete implementations, and let both high- and low-level modules depend on that shared abstraction. Dependency Injection is an implementation *technique*: how you actually get a concrete instance into a class that depends on an interface — via constructor, property, or setter — usually wired up by a DI container. You can apply DIP without DI (manually
new-ing up the interface-typed dependency yourself and passing it in), and you can use a DI container while still violating DIP (injecting a concreteSqlOrderRepositorytype instead of anIOrderRepositoryabstraction). In short: DIP is about *what you depend on*; DI is about *how that dependency gets supplied*. -
How does SOLID relate to testability?
Almost every SOLID violation directly translates into a testing pain point. SRP violations mean a test for one behavior drags in unrelated setup (mocking a database just to test a calculation). DIP violations mean a class hard-wires a concrete dependency (a real HTTP client, a real database), making it untestable without hitting real infrastructure. ISP violations mean mocking an interface requires stubbing out ten methods you don't care about just to satisfy the compiler. OCP done well means you can test new behavior by adding a new Strategy implementation and testing it in isolation, without re-running or risking existing test suites. In interviews, this is a strong way to argue *why* SOLID matters practically rather than reciting definitions: SOLID code is, almost by construction, code that's easy to unit test in isolation.
-
Give a realistic critique of SOLID — when does strict adherence hurt more than it helps?
Over-applying SOLID, especially DIP and ISP, on a small or early-stage codebase produces "interface-itis" — every class gets a matching interface with exactly one implementation, adding indirection with zero present benefit and making the code harder to navigate (you jump through an interface to find the one class that implements it). SRP taken too far fragments cohesive logic into many tiny classes, making it harder to see the overall flow of a use case. In practice, senior engineers apply SOLID pragmatically: introduce the abstraction when there's a real second implementation, a real testing need, or a real volatility point — not preemptively. A good rule of thumb is to let the second use case justify the abstraction, rather than designing it in on day one "just in case." Simplicity and YAGNI are as much design principles as SOLID is.
Practicing for a real interview? Get live AI help with your answers, in the browser, free to start.
Start a free session