SOLID Interview Prep

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.

  1. What is the Single Responsibility Principle, with a before/after example?

    A class should have one reason to change. Before: an Employee class 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 into Employee (data), PayCalculator (pay logic), EmployeeRepository (persistence), and EmployeeReportFormatter (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.

  2. 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 OrderService that 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 importing System.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.

  3. What is the Open/Closed Principle, with a before/after example?

    Software entities should be open for extension but closed for modification. Before: a DiscountCalculator with if (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 touched

    Now DiscountCalculator depends on IDiscountPolicy and 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.

  4. 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 IDiscountPolicy example 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.

  5. 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 of Bird. Before: Bird has a Fly() method; Penguin : Bird overrides it to throw NotSupportedException. Any code that loops over List<Bird> calling Fly() now breaks when a Penguin is in the list, even though it type-checks. After: separate the capability — Bird has no Fly(); IFlyingBird : Bird adds Fly(), implemented only by birds that actually fly, while Penguin just implements Bird. LSP violations are usually a signal that inheritance was used to model an "is-a" relationship that doesn't actually hold behaviorally, only structurally.

  6. Walk through the classic Rectangle/Square LSP violation.

    Square seems like a natural subclass of Rectangle mathematically, so a common (bad) design makes Square override SetWidth/SetHeight to 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 Square and the assertion fails — setting height also changed width, violating an invariant callers reasonably assumed held for any Rectangle. 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.

  7. 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 IWorker interface with Work() and Eat(); a RobotWorker is forced to implement Eat() with a meaningless throw or empty body because it doesn't eat. After: split into IWorkable { Work(); } and IFeedable { Eat(); }; RobotWorker implements only IWorkable, HumanWorker implements 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.

  8. 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: if IRepository has 15 methods and a consumer only needs GetById, 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.

  9. 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: OrderService directly instantiates SqlOrderRepository — the business logic (high-level) is hard-wired to a specific database technology (low-level), so a database change or unit test requires touching OrderService. After:

    class OrderService {
        private readonly IOrderRepository _repo; // abstraction
        public OrderService(IOrderRepository repo) => _repo = repo;
    }

    Now OrderService and SqlOrderRepository both depend on the IOrderRepository abstraction; neither depends on the other directly. This is what makes it possible to swap SqlOrderRepository for InMemoryOrderRepository in tests, or for MongoOrderRepository later, without changing OrderService at all.

  10. 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 concrete SqlOrderRepository type instead of an IOrderRepository abstraction). In short: DIP is about *what you depend on*; DI is about *how that dependency gets supplied*.

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

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