45 OOP Interview Questions and Answers (2026)
45 OOP interview questions covering the four pillars, SOLID principles, and design patterns with real code in Java, Python, JavaScript, and C#. Updated for 2026
On this page
OOP questions show up in almost every technical interview, regardless of which language the role uses. A frontend interview in JavaScript, a backend interview in Java, a systems interview in C++: the four pillars and SOLID principles come up everywhere because they are the shared vocabulary of object-oriented design.
These 45 questions cover the full range: foundational definitions every candidate should nail cleanly, SOLID principles with real violation-and-fix examples, the design patterns that actually come up in interviews, and the practical judgment questions that separate strong candidates from those who only memorized definitions. If you're also preparing for backend roles, our NestJS interview questions guide covers dependency injection and SOLID principles in a framework context, and our Node.js interview questions guide rounds out the backend interview prep series.
Category 1: Fundamentals (Q1-Q8)
Fundamental questions test whether you understand the core vocabulary of object-oriented programming. Every OOP interview starts here, regardless of seniority level.
Q1. What is Object-Oriented Programming and why does it exist?
Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around objects: self-contained units that bundle data (attributes) and behavior (methods) together. Instead of writing a long sequence of instructions that operate on separate data structures (procedural style), OOP models real-world entities as objects that know both what they are and what they can do.
OOP exists to solve problems that procedural programming struggles with at scale: as codebases grow, procedural code tends to accumulate global state and tightly coupled functions that are hard to test, extend, or reason about independently. OOP addresses this through four foundational principles: encapsulation, abstraction, inheritance, and polymorphism, which together let you build software in modular, reusable, and maintainable pieces.
- Modularity: complex problems break into smaller, focused classes
- Reusability: classes and objects are used repeatedly across a codebase
- Maintainability: encapsulation and abstraction limit the blast radius of changes
- Extensibility: polymorphism and inheritance let you add behavior without rewriting existing code
Q2. What is the difference between procedural and object-oriented programming?
Procedural programming organizes code as a sequence of procedures (functions) that operate on data passed between them. Data and the functions that act on it are separate. C is the classic example.
Object-oriented programming bundles data and the functions that operate on it into a single unit, the object. Data is typically hidden from direct outside access (encapsulation) and accessed through methods.
# Procedural style
def calculate_area(width, height):
return width * height
width, height = 5, 10
area = calculate_area(width, height)
# OOP style
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
rect = Rectangle(5, 10)
area = rect.area()The OOP version groups the data (width, height) with the behavior (area calculation) that operates on it. As a codebase grows, this grouping becomes the difference between a maintainable system and a tangle of loosely related functions and global variables.
Q3. What is the difference between a class and an object?
A class is a blueprint or template that defines the structure (attributes) and behavior (methods) that objects of that type will have. A class itself does not occupy memory for instance data until you create an object from it.
An object is an instance of a class: a concrete realization with actual values stored in memory. You can create many objects from a single class, each with its own independent state.
// Class: the blueprint
class Car {
String model;
int year;
void drive() {
System.out.println(model + " is driving");
}
}
// Objects: instances of the blueprint
Car car1 = new Car();
car1.model = "Civic";
car1.year = 2024;
Car car2 = new Car();
car2.model = "Model 3";
car2.year = 2026;The common analogy: a class is like a cookie cutter, and objects are the actual cookies it produces. Each cookie (object) can have its own decorations (state), but they all share the same shape (structure) defined by the cutter (class).
Q4. What are the four pillars of OOP? Give a one-line definition for each.
This is the single most universal opening question across every OOP interview, in every language.
Encapsulation: bundling data and the methods that operate on it into a single unit (a class), and restricting direct access to internal state using access modifiers.
Abstraction: hiding complex implementation details and exposing only the essential features a user needs to interact with an object.
Inheritance: a mechanism that lets one class acquire the properties and behaviors of another class, enabling code reuse and hierarchical relationships.
Polymorphism: the ability of an object, method, or reference to take many forms; the same method call behaves differently depending on context.
A useful way to connect all four: encapsulation enables abstraction (hiding data is how you hide complexity). Inheritance enables polymorphism (you need a shared parent type before you can have one reference behave differently based on the actual object type at runtime).
Q5. What are constructors and what types exist?
A constructor is a special method that initializes a newly created object. It runs automatically when you create an instance and typically has the same name as the class (or a special syntax depending on the language).
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
}
const user = new User("Alice", "alice@example.com");Default constructor: takes no parameters, initializes fields with default values. Most languages generate one automatically if you define no constructor at all.
Parameterized constructor: accepts arguments to initialize fields with specific values at creation time, as shown above.
Copy constructor (C++ specifically): initializes an object using the values of another object of the same class.
class Point {
public:
int x, y;
Point(int x, int y) : x(x), y(y) {} // parameterized
Point(const Point& other) : x(other.x), y(other.y) {} // copy constructor
};Static factory methods are often used as an alternative to multiple overloaded constructors, since they can have descriptive names that plain constructors cannot.
Q6. What are access modifiers and what do they control?
Access modifiers (also called access specifiers) are keywords that control the visibility and accessibility of classes, methods, and fields from outside their defining scope. They are the primary mechanism that enables encapsulation.
public class BankAccount {
private double balance; // accessible only within this class
protected String accountType; // accessible within class and subclasses
public String accountNumber; // accessible from anywhere
private void logTransaction() { /* internal use only */ }
public double getBalance() { return balance; } // controlled access point
}public: accessible from anywhere. private: accessible only within the defining class. protected: accessible within the class and its subclasses. package-private / internal (default in Java, internal in C#): accessible within the same module or package, but not outside it.
Python does not enforce true access modifiers at the language level. A single leading underscore (_balance) is a convention signaling "treat as protected." A double leading underscore (__balance) triggers name mangling, which makes external access awkward but not truly impossible. This is a common interview trap: Python's privacy is convention-based, not enforced by the interpreter.
Q7. What is the this (or self) keyword and what does it refer to?
this (Java, JavaScript, C++, C#) or self (Python) is a reference to the current object instance, the specific object on which a method was called. It lets a method distinguish between its own parameters and the object's fields when they share a name.
class Counter:
def __init__(self, start):
self.count = start # self.count is the instance field,
# start is the local parameter
def increment(self):
self.count += 1
return self.countclass Counter {
constructor(start) {
this.count = start;
}
increment() {
this.count += 1;
return this.count;
}
}A common gotcha in JavaScript specifically: this is not always bound the way developers expect, especially inside regular function callbacks (as opposed to arrow functions, which capture this from their enclosing scope). This distinction is a frequent JavaScript-specific OOP interview question.
Q8. What is the difference between static and instance members?
Instance members (fields and methods) belong to a specific object. Each object gets its own independent copy of instance fields, and instance methods operate on that specific object's data.
Static members belong to the class itself, shared across every instance. There is exactly one copy of a static field, regardless of how many objects exist. Static methods can be called without creating an instance at all.
class Counter {
static int totalCount = 0; // shared across ALL instances
int id; // unique to EACH instance
Counter() {
totalCount++; // increments the shared static field
id = totalCount; // captures this instance's unique id
}
static int getTotalCount() { // static method, no instance needed
return totalCount;
}
}
Counter c1 = new Counter(); // totalCount = 1, c1.id = 1
Counter c2 = new Counter(); // totalCount = 2, c2.id = 2
System.out.println(Counter.getTotalCount()); // 2, called on the classCommon use cases for static members: utility methods that don't need object state (Math.max()), constants shared across all instances, and counters or registries tracking something across every instance of a class.
Category 2: Encapsulation and Abstraction (Q9-Q14)
Encapsulation and abstraction are the two most commonly confused pillars, and interviewers ask about them specifically because they want to see if you understand the distinction. This category covers both individually and the comparison question that trips up most candidates.
Q9. What is encapsulation? Give a concrete example.
Encapsulation is the practice of bundling data and the methods that operate on that data into a single unit (a class), while restricting direct access to the internal state. External code interacts with the object only through a defined public interface (methods), never by reaching in and modifying fields directly.
public class BankAccount {
private double balance; // hidden from direct external access
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
balance += amount;
}
public void withdraw(double amount) {
if (amount > balance) throw new IllegalStateException("Insufficient funds");
balance -= amount;
}
public double getBalance() {
return balance; // read-only access, no direct write access
}
}Without encapsulation, any code anywhere could do account.balance = -500 directly, bypassing every validation rule. With the balance field private and only accessible through deposit()/withdraw()/getBalance(), the class itself guarantees its invariants are never violated by external code.
This is also why encapsulation matters for maintainability: you can later change how balance is stored internally (a different data type, an audit log, a caching layer) without breaking any code that calls deposit() and withdraw(), because the public interface never changed.
Q10. What is abstraction? Give a concrete example.
Abstraction is the practice of hiding complex implementation details and exposing only the essential features a user needs to interact with something. The user knows what an operation does without needing to know how it does it internally.
The classic real-world analogy: a TV remote. You press the power button (the essential interface) without knowing anything about the infrared signal encoding, the circuit design, or the firmware that processes your button press (the hidden complexity).
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount):
pass # the WHAT is defined, the HOW is left to each implementation
class StripeProcessor(PaymentProcessor):
def process_payment(self, amount):
# complex Stripe API integration hidden here
print(f"Processing ${amount} via Stripe")
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount):
# completely different PayPal integration hidden here
print(f"Processing ${amount} via PayPal")
def checkout(processor: PaymentProcessor, amount):
processor.process_payment(amount) # caller doesn't know or care HOWThe checkout function works with any PaymentProcessor without knowing anything about Stripe's or PayPal's actual API. That is abstraction at work: a simple, stable interface hiding arbitrarily complex implementation behind it.
Q11. What is the difference between encapsulation and abstraction?
This is one of the most commonly confused pairs in OOP interviews, and interviewers ask it specifically because so many candidates conflate them.
Encapsulation is about hiding DATA. It binds data and the methods that act on it into one unit and restricts direct access using access modifiers (private, protected). The mechanism is access control.
Abstraction is about hiding COMPLEXITY. It focuses on showing only essential behavior while hiding the underlying implementation details. The mechanism is interfaces and abstract classes.
A simple way to remember it: encapsulation is about HOW you protect data (the mechanism). Abstraction is about WHAT you expose to the user (the design decision).
// Encapsulation: balance is hidden via private + access methods
public class BankAccount {
private double balance; // encapsulation: data hiding
public double getBalance() { return balance; }
}
// Abstraction: the interface defines WHAT, not HOW
interface PaymentMethod {
void pay(double amount); // abstraction: hides implementation complexity
}You can have encapsulation without abstraction (a class with private fields but no meaningful interface design) and abstraction without strict encapsulation (an interface implemented by a class with mostly public fields). In well-designed OOP code, they typically work together.
Q12. What is an abstract class?
An abstract class is a class that cannot be instantiated directly and is intended to be used as a base class for inheritance. It can contain both abstract methods (declared but not implemented) and concrete methods (fully implemented, inherited as-is by subclasses).
abstract class Shape {
abstract double calculateArea(); // abstract: no implementation here
void describe() { // concrete: shared implementation
System.out.println("This shape has an area of " + calculateArea());
}
}
class Circle extends Shape {
double radius;
Circle(double radius) { this.radius = radius; }
double calculateArea() { // must implement the abstract method
return Math.PI * radius * radius;
}
}from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def calculate_area(self):
pass
def describe(self):
print(f"This shape has an area of {self.calculate_area()}")Use an abstract class when subclasses share some common implementation (the describe() method above) in addition to a shared contract. If there is no shared implementation at all, an interface is usually the better choice.
Q13. What is an interface?
An interface defines a contract: a set of method signatures that any implementing class must provide, without specifying any implementation itself. It defines WHAT a class can do, not HOW.
interface Drawable {
void draw();
void resize(double factor);
}
class Square implements Drawable {
double side;
public void draw() {
System.out.println("Drawing a square");
}
public void resize(double factor) {
side *= factor;
}
}interface Drawable {
draw(): void;
resize(factor: number): void;
}
class Square implements Drawable {
side: number;
draw(): void {
console.log("Drawing a square");
}
resize(factor: number): void {
this.side *= factor;
}
}A class can implement multiple interfaces (achieving a form of multiple inheritance for behavior contracts) even in languages like Java and C# that forbid inheriting from multiple classes. This is precisely why "favor interfaces over abstract classes when you need multiple inheritance of behavior" is common interview advice.
Q14. What is the difference between an abstract class and an interface?
| Abstract Class | Interface | |
|---|---|---|
| Implementation | Can mix abstract and concrete methods | Traditionally no implementation (modern Java/C# allow default methods) |
| Inheritance | A class can extend only ONE abstract class | A class can implement MULTIPLE interfaces |
| Fields | Can have instance fields with state | Traditionally no instance state (only constants) |
| Constructors | Can have constructors | Cannot have constructors |
| Use case | Shared base behavior + some shared implementation | Pure contract, no shared implementation needed |
The decision rule most interviewers want to hear: use an abstract class when subclasses share meaningful common implementation and a clear IS-A hierarchy. Use an interface when you need to define a capability or contract that unrelated classes might implement differently (a Flyable interface implemented by both Bird and Airplane, which have nothing else in common).
Modern languages have blurred this distinction somewhat. Java 8+ interfaces support default methods with implementation. C# 8+ also supports default interface implementations. The conceptual distinction (single inheritance hierarchy vs. multiple capability contracts) still matters even as the syntax has converged.
Category 3: Inheritance (Q15-Q21)
Inheritance questions test whether you understand code reuse hierarchies, the trade-offs of IS-A vs HAS-A relationships, and the specific pitfalls (like the diamond problem) that shape how modern languages handle inheritance.
Q15. What is inheritance? Give a concrete example.
Inheritance is a mechanism where one class (the subclass, or derived/child class) acquires the properties and behaviors of another class (the superclass, or base/parent class). It models an IS-A relationship and enables code reuse.
class Vehicle {
int speed;
String fuelType;
void start() {
System.out.println("Vehicle starting");
}
}
class Car extends Vehicle { // Car IS-A Vehicle
int numberOfDoors;
void honk() {
System.out.println("Beep beep");
}
}
Car myCar = new Car();
myCar.speed = 120; // inherited from Vehicle
myCar.start(); // inherited from Vehicle
myCar.honk(); // defined in Car itselfCar automatically gets speed, fuelType, and start() from Vehicle without redefining them, while adding its own numberOfDoors field and honk() method. This is the core value of inheritance: shared behavior written once in the parent, reused across every child.
Q16. What types of inheritance exist?
Single inheritance: a class inherits from exactly one parent class. Animal → Dog
Multilevel inheritance: a chain of inheritance across more than two levels. Animal → Mammal → Dog
Hierarchical inheritance: multiple classes inherit from the same single parent class. Animal → Dog, Cat, Bird
Multiple inheritance: a class inherits from more than one parent class directly. Supported by C++ and Python for classes, but explicitly disallowed for classes in Java and C# (though both allow implementing multiple interfaces, which is a different mechanism).
Hybrid inheritance: a combination of two or more of the above types in the same hierarchy.
The interview-relevant point: knowing the type names matters less than understanding WHY Java and C# block multiple class inheritance (covered in Q19, the diamond problem) while still allowing multiple interface implementation.
Q17. What is the difference between IS-A and HAS-A relationships?
IS-A describes an inheritance relationship. A subclass IS-A specialized version of its parent class.
HAS-A describes a composition or association relationship. A class contains, or has, an instance of another class as a field, without being a specialized version of it.
class Dog extends Animal { } // Dog IS-A Animal
class Car {
Engine engine; // Car HAS-A Engine (composition)
Driver driver; // Car HAS-A Driver (association)
}A Car is not a specialized Engine, inheritance would be the wrong relationship here. Instead, a Car contains an Engine as a component. This distinction is the foundation of the next question, composition vs inheritance.
Q18. What is "favor composition over inheritance" and why is it good advice?
This is one of the most repeated pieces of OOP design advice, and interviewers often ask candidates to explain WHY, not just recite it.
Inheritance creates tight coupling: a subclass is bound to its parent's implementation details, and changes to the parent can unexpectedly break subclasses (the "fragile base class" problem). Deep inheritance hierarchies become hard to reason about and hard to change.
Composition builds behavior by combining smaller, focused objects as fields (HAS-A) rather than through class hierarchies (IS-A). It is more flexible because you can swap components at runtime.
// Inheritance approach: rigid, breaks if behavior needs to change at runtime
class FlyingCar extends Car { void fly() { /* ... */ } }
// Composition approach: flexible, swap behavior via composed objects
class Car {
private FlightCapability flightCapability;
Car(FlightCapability capability) {
this.flightCapability = capability;
}
void fly() {
if (flightCapability != null) flightCapability.fly();
}
}The composition version lets you test Car with a mock FlightCapability, swap implementations without subclassing, and avoid a rigid class hierarchy that grows combinatorially as you add more optional behaviors (FlyingCar, SwimmingCar, FlyingSwimmingCar...). This combinatorial explosion problem is exactly what design patterns like Decorator and Strategy solve, covered later in this guide.
Q19. What is the diamond problem and how do different languages solve it?
The diamond problem occurs in multiple inheritance when a class inherits from two classes that both inherit from a common ancestor, creating ambiguity about which version of an inherited method should be used.
If both Mammal and Bird override a method from Animal differently, which version does Platypus (which extends both) get? This ambiguity is the diamond problem.
C++ allows multiple inheritance and requires the programmer to resolve the ambiguity explicitly (via virtual inheritance or explicit scope resolution), putting the burden on the developer.
Java and C# sidestep the problem entirely by disallowing multiple inheritance for classes. A class extends exactly one parent class. They still allow implementing multiple interfaces, but since interfaces traditionally had no implementation, there was historically no ambiguity to resolve (modern default interface methods reintroduce a milder version of this question, resolved by requiring the implementing class to explicitly override the conflicting method).
Python allows multiple inheritance and resolves ambiguity using the C3 linearization algorithm (Method Resolution Order, or MRO), which determines a single, deterministic order to search for methods across the inheritance graph.
Q20. Can you call a base class method without creating an instance?
Yes, in three specific cases.
Static methods: static methods belong to the class, not any instance, and can always be called without instantiation.
class MathUtils {
static int square(int x) { return x * x; }
}
MathUtils.square(5); // no instance neededThrough a subclass constructor before an instance fully exists: a subclass's constructor implicitly or explicitly calls the parent constructor (super() in Java, super().__init__() in Python) before the subclass instance is fully formed.
Via a static factory method on the base class itself, which internally creates and returns an instance without the caller directly invoking new.
Q21. What is method hiding and how does it differ from method overriding?
This is a Java/C# nuance that frequently trips up candidates who assume all "same name, same signature" cases behave like overriding.
Method overriding applies to instance methods and uses dynamic (runtime) dispatch: the actual object's type determines which version runs, regardless of the reference type used to call it.
Method hiding applies to static methods. Static methods are NOT polymorphic. Which version runs is determined by the reference TYPE at compile time, not the actual object type at runtime.
class Parent {
static void staticMethod() { System.out.println("Parent static"); }
void instanceMethod() { System.out.println("Parent instance"); }
}
class Child extends Parent {
static void staticMethod() { System.out.println("Child static"); }
void instanceMethod() { System.out.println("Child instance"); }
}
Parent p = new Child();
p.staticMethod(); // prints "Parent static" (resolved by reference TYPE: hiding)
p.instanceMethod(); // prints "Child instance" (resolved by actual object TYPE: overriding)This is a favorite "gotcha" interview question precisely because the output surprises people who haven't internalized that static methods cannot participate in true runtime polymorphism.
Category 4: Polymorphism (Q22-Q27)
Polymorphism questions test whether you understand how the same method call can produce different behavior, and whether you can distinguish between compile-time and runtime resolution. The overloading vs overriding comparison is one of the single most asked OOP questions across all languages.
Q22. What is polymorphism? Give a concrete example.
Polymorphism, literally "many forms," is the ability of an object, method, or reference to behave differently depending on context. The same method call can produce different behavior depending on the actual type of the object it's called on.
class Shape {
void draw() { System.out.println("Drawing a generic shape"); }
}
class Circle extends Shape {
void draw() { System.out.println("Drawing a circle"); }
}
class Square extends Shape {
void draw() { System.out.println("Drawing a square"); }
}
Shape[] shapes = { new Circle(), new Square() };
for (Shape s : shapes) {
s.draw(); // calls Circle's draw() or Square's draw() depending on actual type
}
// Output: "Drawing a circle" then "Drawing a square"The loop doesn't know or care whether each shape is a Circle or a Square. It just calls draw() on a Shape reference, and the correct implementation runs automatically based on the object's actual type. This is the practical value of polymorphism: code that works against an abstraction (Shape) without needing to know every concrete type it might encounter.
Q23. What is the difference between compile-time and runtime polymorphism?
Compile-time polymorphism (also called static binding or early binding) is resolved by the compiler before the program runs. Method overloading is the classic example: the compiler determines which overloaded version to call based on the argument types at the call site.
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; } // overload
}
// The compiler decides which add() to call based on argument typesRuntime polymorphism (dynamic binding or late binding) is resolved while the program is executing, based on the actual type of the object, not the reference type. Method overriding is the classic example, as shown in Q22.
The key distinguishing question interviewers ask: "what determines which method runs?" For compile-time polymorphism, it's the compiler analyzing argument types. For runtime polymorphism, it's the JVM/runtime checking the actual object's type via a virtual method table at the moment the call executes.
Q24. What is the difference between method overloading and method overriding?
This is consistently one of the single most asked OOP questions across every language and every seniority level.
Method overloading: defining multiple methods with the SAME name but DIFFERENT parameter lists (different number, types, or order of parameters), within the same class. Resolved at compile time.
class Printer {
void print(String text) { System.out.println(text); }
void print(int number) { System.out.println(number); }
void print(String text, int times) {
for (int i = 0; i < times; i++) System.out.println(text);
}
}Method overriding: redefining a method that already exists in a parent class, in a subclass, with the SAME name AND SAME parameter list. Resolved at runtime based on the actual object type.
class Animal {
void makeSound() { System.out.println("Some generic sound"); }
}
class Dog extends Animal {
@Override
void makeSound() { System.out.println("Bark"); } // same signature, new behavior
}| Overloading | Overriding | |
|---|---|---|
| Relationship | Same class | Parent-child classes |
| Parameters | Must differ | Must be identical |
| Binding | Compile-time (static) | Runtime (dynamic) |
| Purpose | Multiple ways to call similar logic | Specialize inherited behavior |
Q25. What is operator overloading?
Operator overloading lets you redefine what built-in operators (+, -, ==, etc.) do when applied to instances of a custom class, giving custom types intuitive, mathematical-style syntax.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other): # overloads the + operator
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2 # calls __add__ under the hood
print(v3) # Vector(4, 6)class Vector {
public:
int x, y;
Vector(int x, int y) : x(x), y(y) {}
Vector operator+(const Vector& other) const {
return Vector(x + other.x, y + other.y);
}
};Java and JavaScript notably do NOT support operator overloading for custom classes (with limited exceptions, like Java's + working for String concatenation as a special built-in case). This is a common "language-specific gotcha" interview question: knowing that C++ and Python support custom operator overloading while Java deliberately does not (a design decision intended to keep operator behavior predictable) signals real cross-language fluency.
Q26. What is dynamic method dispatch?
Dynamic method dispatch is the mechanism that makes runtime polymorphism (method overriding) actually work under the hood. When you call a method on an object through a parent-type reference, the runtime looks up the actual object's type and resolves the call to that type's specific implementation, rather than the reference type's implementation.
Animal a = new Dog(); // reference type Animal, actual object type Dog
a.makeSound(); // dynamic dispatch resolves this to Dog's makeSound()Internally (in Java/C#), this is implemented via a virtual method table (vtable): each class has a table mapping method signatures to their actual implementation addresses, and the JVM/CLR follows the table belonging to the OBJECT's actual class, not the reference's declared type, when the call executes.
This mechanism is exactly why static methods (Q21) cannot participate: static methods have no entry in the virtual method table at all, since they aren't called on an object instance through dynamic dispatch in the first place.
Q27. What are covariant return types?
A covariant return type allows an overriding method in a subclass to return a more specific (narrower) type than the parent method declares, as long as the narrower type is still compatible with (a subtype of) the original declared return type.
class Animal {
Animal reproduce() { return new Animal(); }
}
class Dog extends Animal {
@Override
Dog reproduce() { // returns Dog, narrower than Animal
return new Dog();
}
}This is legal because anywhere code expects an Animal back from reproduce(), receiving a Dog (which IS an Animal) satisfies that expectation perfectly. Covariant return types let overriding methods be more specific and useful to callers who know the concrete subtype they're working with, without breaking the contract established by the parent class.
Not every language supports this. Java and C++ support covariant return types; earlier Java versions (pre-5) did not, which is worth knowing if asked about legacy codebases.
Category 5: SOLID Principles (Q28-Q33)
SOLID principles are where interviewers separate candidates who understand OOP definitions from those who can apply design judgment. Each principle is covered below with a violation and a fix, which is exactly the format most interviewers expect. If you're preparing for framework-specific roles, our NestJS interview questions guide shows how Dependency Inversion (Q33) becomes dependency injection in practice.
Q28. What does SOLID stand for and why does it matter?
SOLID is an acronym for five object-oriented design principles, popularized by Robert C. Martin, that guide developers toward maintainable, extensible software architecture:
S: Single Responsibility Principle. O: Open/Closed Principle. L: Liskov Substitution Principle. I: Interface Segregation Principle. D: Dependency Inversion Principle.
These are not language-specific syntax rules. They are design guidelines that apply regardless of which OOP language you're using. Interviewers ask about SOLID specifically to assess design judgment beyond syntax knowledge: can you recognize when code violates good design, and can you explain how to fix it.
Q29. Explain the Single Responsibility Principle with an example.
A class should have only one reason to change. Each class should be responsible for a single, well-defined piece of functionality.
// VIOLATION: this class has two responsibilities, two reasons to change
class UserService {
void registerUser(String email, String password) {
saveToDatabase(email, password);
sendWelcomeEmail(email); // this is a DIFFERENT responsibility
}
void saveToDatabase(String email, String password) { /* ... */ }
void sendWelcomeEmail(String email) { /* ... */ }
}
// FIX: split into two classes, each with a single responsibility
class UserService {
private EmailService emailService;
void registerUser(String email, String password) {
saveToDatabase(email, password);
emailService.sendWelcomeEmail(email);
}
void saveToDatabase(String email, String password) { /* ... */ }
}
class EmailService {
void sendWelcomeEmail(String email) { /* ... */ }
}If email-sending logic changes (a new provider, a template change), only EmailService needs to change. UserService's persistence logic is unaffected. This separation is what makes the codebase easier to test, change, and understand independently.
Q30. Explain the Open/Closed Principle with an example.
Software entities (classes, modules, functions) should be open for extension but closed for modification. You should be able to add new behavior without changing existing, already-tested code.
// VIOLATION: adding a new shape requires modifying this existing method
class AreaCalculator {
double calculateArea(Object shape) {
if (shape instanceof Circle) {
return Math.PI * ((Circle) shape).radius * ((Circle) shape).radius;
} else if (shape instanceof Square) {
return ((Square) shape).side * ((Square) shape).side;
}
// every new shape requires editing THIS method again
return 0;
}
}
// FIX: each shape implements its own area calculation
interface Shape {
double calculateArea();
}
class Circle implements Shape {
double radius;
public double calculateArea() { return Math.PI * radius * radius; }
}
class Square implements Shape {
double side;
public double calculateArea() { return side * side; }
}
class AreaCalculator {
double calculateArea(Shape shape) {
return shape.calculateArea(); // never needs modification for new shapes
}
}Adding a Triangle class now requires zero changes to AreaCalculator. You extend the system by adding a new class, not by modifying existing, already-tested logic.
Q31. Explain the Liskov Substitution Principle with an example.
Objects of a subclass should be substitutable for objects of the parent class without breaking the program's correctness. If code works correctly with a base class reference, it should continue to work correctly when given any subclass instance instead.
// VIOLATION: Square breaks the contract established by Rectangle
class Rectangle {
protected double width, height;
void setWidth(double w) { this.width = w; }
void setHeight(double h) { this.height = h; }
double getArea() { return width * height; }
}
class Square extends Rectangle {
@Override
void setWidth(double w) {
this.width = w;
this.height = w; // forces height to match, breaks expectations
}
@Override
void setHeight(double h) {
this.width = h;
this.height = h;
}
}
// Code that works fine with a Rectangle breaks silently with a Square:
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
// Caller expects area = 50, but gets 100A Square IS mathematically a special Rectangle, but modeling it as a subclass here violates LSP because it silently changes the behavior callers rely on. The fix is usually to NOT model this relationship through inheritance at all: make both Rectangle and Square independent implementations of a shared Shape interface, with no inheritance relationship between them.
Q32. Explain the Interface Segregation Principle with an example.
Clients should not be forced to depend on methods they do not use. Prefer several small, specific interfaces over one large, general-purpose interface.
// VIOLATION: a single bloated interface forces irrelevant methods on every implementer
interface Worker {
void work();
void eat();
void sleep();
}
class Robot implements Worker {
public void work() { /* ... */ }
public void eat() { throw new UnsupportedOperationException(); } // robots don't eat
public void sleep() { throw new UnsupportedOperationException(); }
}
// FIX: split into focused, specific interfaces
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
class Human implements Workable, Eatable, Sleepable { /* implements all three */ }
class Robot implements Workable { /* implements only what's relevant */ }The violation forces Robot to implement methods that make no sense for it, leading to exception-throwing stubs, a clear design smell. The fix lets each class implement only the interfaces that are actually meaningful to it.
Q33. Explain the Dependency Inversion Principle with an example.
High-level modules should not depend on low-level modules directly; both should depend on abstractions. This principle is the foundation of dependency injection, which is why it shows up constantly in framework interview questions (NestJS, Spring, Angular).
// VIOLATION: NotificationService is tightly coupled to a concrete EmailSender
class EmailSender {
void send(String message) { /* SMTP logic */ }
}
class NotificationService {
private EmailSender emailSender = new EmailSender(); // hard dependency
void notify(String message) {
emailSender.send(message);
}
}
// FIX: depend on an abstraction, inject the concrete implementation
interface MessageSender {
void send(String message);
}
class EmailSender implements MessageSender {
public void send(String message) { /* SMTP logic */ }
}
class SmsSender implements MessageSender {
public void send(String message) { /* SMS gateway logic */ }
}
class NotificationService {
private MessageSender sender;
NotificationService(MessageSender sender) { // dependency injected
this.sender = sender;
}
void notify(String message) {
sender.send(message);
}
}
// Swapping implementations requires zero changes to NotificationService
NotificationService emailNotifier = new NotificationService(new EmailSender());
NotificationService smsNotifier = new NotificationService(new SmsSender());This is the principle underlying every dependency injection framework. NotificationService depends only on the MessageSender abstraction, never on a specific concrete implementation, which is exactly what makes it testable (inject a mock sender) and extensible (add new sender types without touching this class at all).
Category 6: Design Patterns (Q34-Q42)
Design pattern questions test whether you can recognize recurring problem shapes and apply proven solutions. Interviewers care more about when and why you would reach for a pattern than whether you can recite its textbook definition.
Q34. What is a design pattern and what are the three main categories?
A design pattern is a reusable, well-tested template solution to a commonly occurring software design problem. Patterns are not finished code you copy-paste; they are proven approaches you adapt to your specific situation.
The 23 foundational patterns documented in the 1994 "Gang of Four" book (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides) are grouped into three categories:
Creational patterns: deal with object creation mechanisms, abstracting away how and when objects get instantiated. Examples: Singleton, Factory, Abstract Factory, Builder, Prototype.
Structural patterns: deal with how classes and objects are composed to form larger structures, defining relationships between entities. Examples: Adapter, Decorator, Facade, Proxy, Composite.
Behavioral patterns: deal with how objects communicate and interact with each other, distributing responsibility between objects. Examples: Observer, Strategy, Command, State, Template Method.
Q35. What is the Singleton pattern and what are its drawbacks?
The Singleton pattern ensures a class has only one instance throughout the application and provides a single global access point to it. Common use cases: configuration managers, logging, database connection pools.
public class ConfigManager {
private static ConfigManager instance;
private ConfigManager() {} // private constructor prevents external instantiation
public static synchronized ConfigManager getInstance() {
if (instance == null) {
instance = new ConfigManager();
}
return instance;
}
}The synchronized keyword matters here: in a multi-threaded environment, a non-synchronized check-then-create pattern can let two threads both pass the instance == null check before either has created the object, resulting in two instances, which defeats the entire purpose of Singleton. This thread-safety gotcha is one of the most commonly asked Singleton follow-up questions.
- Global state: introduces global state, making unit testing harder (dependencies can't be easily mocked or substituted)
- Single Responsibility violation: the class manages both its business logic AND its own lifecycle/instantiation
- Hidden dependencies: code that calls
ConfigManager.getInstance()doesn't make its dependency explicit through its constructor or parameters
Many modern codebases prefer dependency injection over the classic Singleton pattern specifically to avoid these testing and coupling problems.
Q36. What is the Factory pattern and how does it differ from Abstract Factory?
The Factory pattern provides a method for creating objects without exposing the exact instantiation logic to the caller. It centralizes object creation behind a single method.
interface Vehicle { void drive(); }
class Car implements Vehicle { public void drive() { System.out.println("Driving a car"); } }
class Motorcycle implements Vehicle { public void drive() { System.out.println("Riding a motorcycle"); } }
class VehicleFactory {
static Vehicle createVehicle(String type) {
if (type.equals("car")) return new Car();
if (type.equals("motorcycle")) return new Motorcycle();
throw new IllegalArgumentException("Unknown vehicle type");
}
}
Vehicle v = VehicleFactory.createVehicle("car");The Abstract Factory pattern goes one level higher: it provides an interface for creating FAMILIES of related objects, without specifying their concrete classes, by using multiple related factory methods grouped together.
interface UIFactory {
Button createButton();
Checkbox createCheckbox();
}
class DarkThemeFactory implements UIFactory {
public Button createButton() { return new DarkButton(); }
public Checkbox createCheckbox() { return new DarkCheckbox(); }
}
class LightThemeFactory implements UIFactory {
public Button createButton() { return new LightButton(); }
public Checkbox createCheckbox() { return new LightCheckbox(); }
}The distinction interviewers want to hear: Factory creates ONE type of product through a single method. Abstract Factory creates FAMILIES of related products that need to stay consistent with each other (a dark button should always pair with a dark checkbox, never a light one).
Q37. What is the Builder pattern and when do you use it?
The Builder pattern constructs a complex object step by step, separating the construction process from the object's final representation. It solves the problem of constructors with too many parameters, especially many optional ones.
class Pizza {
private final int size;
private final boolean cheese, pepperoni, mushrooms;
private Pizza(Builder builder) {
this.size = builder.size;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.mushrooms = builder.mushrooms;
}
static class Builder {
private int size;
private boolean cheese, pepperoni, mushrooms;
Builder size(int size) { this.size = size; return this; }
Builder cheese(boolean v) { this.cheese = v; return this; }
Builder pepperoni(boolean v) { this.pepperoni = v; return this; }
Builder mushrooms(boolean v) { this.mushrooms = v; return this; }
Pizza build() { return new Pizza(this); }
}
}
Pizza pizza = new Pizza.Builder()
.size(12)
.cheese(true)
.pepperoni(true)
.build(); // self-documenting, readable, only set what you needUse the Builder pattern when an object has many optional fields, when construction involves multiple steps that need to happen in a specific order, or when you want to produce different representations using the same construction process.
Q38. What is the Observer pattern and when do you use it?
The Observer pattern defines a one-to-many dependency between objects: when one object (the subject) changes state, all its dependent objects (observers) are automatically notified, without the subject needing to know any details about what its observers actually do with that notification.
interface Observer {
void update(String message);
}
class Subject {
private List<Observer> observers = new ArrayList<>();
void attach(Observer observer) { observers.add(observer); }
void detach(Observer observer) { observers.remove(observer); }
void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
class EmailNotifier implements Observer {
public void update(String message) {
System.out.println("Emailing: " + message);
}
}
class SmsNotifier implements Observer {
public void update(String message) {
System.out.println("Texting: " + message);
}
}
Subject orderSystem = new Subject();
orderSystem.attach(new EmailNotifier());
orderSystem.attach(new SmsNotifier());
orderSystem.notifyObservers("Order shipped!");This is a top-tier interview pattern because it shows up constantly in real systems: a stock price changes and multiple displays need to update; a user places an order and inventory, notifications, and analytics all need to react. Look for the words "notify" or "update multiple components" in a system design prompt as a strong signal that Observer is the right answer.
JavaScript's event listeners, React's state subscriptions, and Java's built-in Observer-pattern-adjacent libraries are all real-world applications of this exact pattern.
Q39. What is the Strategy pattern and when do you use it?
The Strategy pattern defines a family of interchangeable algorithms, encapsulates each one in its own class, and lets the algorithm be selected and swapped at runtime. It replaces sprawling if/else or switch chains with clean polymorphic delegation.
interface PaymentStrategy {
void pay(double amount);
}
class CreditCardPayment implements PaymentStrategy {
public void pay(double amount) {
System.out.println("Paid $" + amount + " via credit card");
}
}
class PayPalPayment implements PaymentStrategy {
public void pay(double amount) {
System.out.println("Paid $" + amount + " via PayPal");
}
}
class ShoppingCart {
private PaymentStrategy paymentStrategy;
void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
void checkout(double amount) {
paymentStrategy.pay(amount);
}
}
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(99.99);
cart.setPaymentStrategy(new PayPalPayment()); // swap strategy on the fly
cart.checkout(49.99);If a problem description has words like "different ways to do X" or mentions choosing between several interchangeable behaviors at runtime (sorting algorithms, pricing strategies, payment methods), Strategy is usually the answer.
The frequent follow-up question: "How is this different from the State pattern?" Strategy is about choosing between interchangeable behaviors explicitly set by the calling code. State is about an object's behavior changing automatically based on its own internal state transitions (a vending machine behaving differently in "idle," "selecting," and "dispensing" states).
Q40. What is the Decorator pattern and when do you use it?
The Decorator pattern adds new behavior to an object dynamically, at runtime, by wrapping it in one or more decorator objects, without modifying the original object's class or creating a combinatorial explosion of subclasses.
interface Coffee {
double cost();
String description();
}
class SimpleCoffee implements Coffee {
public double cost() { return 2.0; }
public String description() { return "Coffee"; }
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}
class MilkDecorator extends CoffeeDecorator {
MilkDecorator(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 0.5; }
public String description() { return coffee.description() + " + Milk"; }
}
class SugarDecorator extends CoffeeDecorator {
SugarDecorator(Coffee coffee) { super(coffee); }
public double cost() { return coffee.cost() + 0.2; }
public String description() { return coffee.description() + " + Sugar"; }
}
Coffee order = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
System.out.println(order.description() + " = $" + order.cost());
// "Coffee + Milk + Sugar = $2.7"Without Decorator, supporting every combination of milk, sugar, and whipped cream would require a separate subclass for each combination (CoffeeWithMilk, CoffeeWithMilkAndSugar, CoffeeWithMilkAndSugarAndCream...). Decorator wraps and stacks behaviors instead, avoiding that combinatorial explosion entirely. This is the same problem the "favor composition over inheritance" principle (Q18) addresses, and Decorator is a concrete pattern implementing that advice.
Q41. What is the Adapter pattern and when do you use it?
The Adapter pattern converts the interface of one class into an interface that another part of your code expects, allowing otherwise-incompatible classes to work together without modifying either one's source code.
// Existing, incompatible interface (perhaps from a third-party library)
class LegacyPrinter {
void printOldFormat(String text) {
System.out.println("[Legacy] " + text);
}
}
// The interface your new code expects
interface ModernPrinter {
void print(String text);
}
// Adapter bridges the gap without modifying LegacyPrinter
class PrinterAdapter implements ModernPrinter {
private LegacyPrinter legacyPrinter;
PrinterAdapter(LegacyPrinter legacyPrinter) {
this.legacyPrinter = legacyPrinter;
}
public void print(String text) {
legacyPrinter.printOldFormat(text); // translates the call
}
}
ModernPrinter printer = new PrinterAdapter(new LegacyPrinter());
printer.print("Hello"); // your code only talks to the ModernPrinter interfaceThe most common real-world trigger for this pattern: integrating a third-party library or legacy system whose API doesn't match what your application expects, and you cannot (or shouldn't) modify the third-party code directly. Adapter lets you isolate that mismatch in one place instead of scattering compatibility workarounds throughout your codebase.
Q42. When should you NOT use a design pattern?
This is a senior-level judgment question, and a thoughtful answer here signals real experience beyond memorized definitions.
The problem is simple enough that the pattern adds more complexity than it removes. Using Singleton for a basic global counter when a single static variable would do the job is classic over-engineering.
You're applying a pattern speculatively, "just in case" requirements change. This leads to unused abstraction layers (a Factory for a type that will never have a second implementation) and unnecessary indirection that makes the code harder, not easier, to follow.
The pattern doesn't actually fit the problem shape. Forcing a Strategy pattern onto a system with only one possible behavior, or building an Observer setup for a one-time event with a single static listener, adds ceremony without benefit.
The best interview answer ties this back to YAGNI ("You Aren't Gonna Need It") and the broader principle that design patterns are tools for solving specific recurring problems, not a checklist to apply everywhere. The goal is maintainable code, not pattern usage for its own sake.
Category 7: Practical and Language-Specific (Q43-Q45)
Practical questions test whether you can apply everything above to a real scenario, spot common interview mistakes, and demonstrate cross-language awareness.
Q43. How would you design a parking lot system using OOP principles?
This type of open-ended design scenario is common in mid-to-senior interviews specifically to see how candidates apply the pillars and SOLID principles together, not just recite definitions.
A strong answer walks through the design out loud. Start by identifying the core entities: ParkingLot, ParkingSpot, Vehicle, Ticket, PaymentProcessor.
Apply abstraction: define a Vehicle base type (or interface) with Car, Motorcycle, Truck as concrete types, since a parking spot's compatibility logic shouldn't need to know every vehicle type individually.
abstract class Vehicle {
abstract String getType();
abstract int getSizeRequired();
}
class Car extends Vehicle {
String getType() { return "Car"; }
int getSizeRequired() { return 1; }
}
class Truck extends Vehicle {
String getType() { return "Truck"; }
int getSizeRequired() { return 3; }
}Apply encapsulation: ParkingSpot hides its internal occupied/free state behind methods (assignVehicle(), removeVehicle(), isAvailable()) rather than exposing raw boolean fields for direct mutation.
Apply the Strategy pattern for payment, since payment method (cash, card, mobile) is exactly the "interchangeable algorithm selected at runtime" shape that Strategy solves (see Q39).
Apply the Single Responsibility Principle: ParkingLot manages spot allocation, Ticket manages billing duration and cost calculation, PaymentProcessor (via Strategy) handles payment, each with one clear job.
Apply Dependency Inversion: ParkingLot depends on a PaymentStrategy interface, not a concrete payment class, making it easy to test and extend with new payment types later.
The interviewer is evaluating whether you reach for the right principles naturally as the design grows, not whether you produce a "perfect" UML diagram in 15 minutes.
Q44. What are common mistakes candidates make in OOP interviews?
- Confusing encapsulation with abstraction (Q11) is the single most common mix-up, since both involve "hiding" something but for different purposes and through different mechanisms.
- Treating inheritance as the default tool for code reuse. Many candidates reach for
extendsimmediately, when composition is frequently the better choice (Q18), especially when the relationship isn't a clean IS-A. - Reciting pattern definitions without explaining WHEN to use them. Interviewers consistently say they care more about "I'd use Observer here because multiple components need to react to one event" than a memorized textbook definition.
- Forgetting that static methods don't participate in runtime polymorphism (Q21, Q26), leading to wrong answers on "what does this code print" trick questions.
- Not acknowledging trade-offs. A strong answer on Singleton (Q35) names the thread-safety and testability drawbacks unprompted, rather than only describing the upside.
- Treating SOLID principles as abstract trivia rather than practical refactoring tools. The strongest answers (Q29-Q33) show a violation and a fix, demonstrating the principle changes real code.
Q45. What OOP quirks differ meaningfully across Java, Python, JavaScript, and C#?
This kind of cross-language question tests genuine breadth rather than deep expertise in just one ecosystem.
| Feature | Java / C# | Python | JavaScript |
|---|---|---|---|
| Access modifiers | Enforced at compile time (private, protected, public) | Convention only (_protected, __private triggers name mangling) | True private fields only since #fieldName syntax; before that, convention only |
| Multiple inheritance | Forbidden for classes; allowed for interfaces | Allowed for classes (uses C3 linearization / MRO) | Not applicable (prototype-based, not class-based) |
| Operator overloading | Not supported for custom classes (Java design decision) | Fully supported via __add__, __eq__, etc. | Not supported for custom classes |
| Inheritance model | Classical, class-based inheritance | Classical, class-based inheritance | Prototype-based (class keyword is syntactic sugar over prototype chain) |
| Duck typing | Statically typed; requires explicit interface implementation | Dynamically typed; duck typing is the norm | Dynamically typed; duck typing is the norm |
The inheritance model distinction is particularly important: JavaScript's class keyword introduced in ES6 is syntactic sugar over the same prototype chain that existed since the language's earliest versions. Code that works perfectly well explaining inheritance in Java can describe the wrong mental model entirely if applied uncritically to how JavaScript actually resolves property lookups at runtime.
Quick Reference: All 45 Questions at a Glance
| # | Question | Core Concept |
|---|---|---|
| Q1 | What is OOP and why does it exist | Modularity, reusability, maintainability |
| Q2 | Procedural vs OOP | Functions+data separate vs bundled in objects |
| Q3 | Class vs object | Blueprint vs instance |
| Q4 | Four pillars of OOP | Encapsulation, Abstraction, Inheritance, Polymorphism |
| Q5 | Constructors and types | Default, parameterized, copy constructor |
| Q6 | Access modifiers | public, private, protected, package-private |
| Q7 | this/self keyword | Reference to current instance |
| Q8 | Static vs instance members | Shared across class vs unique per object |
| Q9 | Encapsulation with example | Data hiding via access modifiers + methods |
| Q10 | Abstraction with example | Hiding complexity, exposing essential interface |
| Q11 | Encapsulation vs Abstraction | Hides data (HOW) vs hides complexity (WHAT) |
| Q12 | Abstract class | Cannot instantiate, mix abstract+concrete methods |
| Q13 | Interface | Pure contract, no implementation |
| Q14 | Abstract class vs Interface | Single inheritance vs multiple, fields vs no state |
| Q15 | Inheritance with example | IS-A relationship, code reuse |
| Q16 | Types of inheritance | Single, multilevel, hierarchical, multiple, hybrid |
| Q17 | IS-A vs HAS-A | Inheritance vs composition/association |
| Q18 | Favor composition over inheritance | Flexibility, avoids fragile base class problem |
| Q19 | Diamond problem | Multiple inheritance ambiguity, language solutions |
| Q20 | Calling base method without instance | Static methods, constructor chaining, factory methods |
| Q21 | Method hiding vs overriding | Static (compile-time) vs instance (runtime) resolution |
| Q22 | Polymorphism with example | Same call, different behavior by actual type |
| Q23 | Compile-time vs runtime polymorphism | Overloading (static) vs overriding (dynamic) |
| Q24 | Method overloading vs overriding | Same class diff params vs parent-child same signature |
| Q25 | Operator overloading | Custom operator behavior; C++/Python yes, Java no |
| Q26 | Dynamic method dispatch | Virtual method table, runtime type lookup |
| Q27 | Covariant return types | Subclass override returns narrower compatible type |
| Q28 | SOLID acronym and purpose | 5 design principles for maintainable architecture |
| Q29 | Single Responsibility Principle | One reason to change, violation/fix example |
| Q30 | Open/Closed Principle | Extend without modifying, violation/fix example |
| Q31 | Liskov Substitution Principle | Subclass substitutable for parent, violation/fix |
| Q32 | Interface Segregation Principle | Small focused interfaces, violation/fix example |
| Q33 | Dependency Inversion Principle | Depend on abstractions, foundation of DI |
| Q34 | Design pattern categories | Creational, Structural, Behavioral |
| Q35 | Singleton pattern + drawbacks | One instance, thread-safety, testability issues |
| Q36 | Factory vs Abstract Factory | Single product vs families of related products |
| Q37 | Builder pattern | Step-by-step construction, many optional fields |
| Q38 | Observer pattern | One-to-many notification, attach/notify |
| Q39 | Strategy pattern | Interchangeable algorithms swapped at runtime |
| Q40 | Decorator pattern | Stack behaviors dynamically, avoid subclass explosion |
| Q41 | Adapter pattern | Bridge incompatible interfaces without modifying either |
| Q42 | When NOT to use a design pattern | Over-engineering, YAGNI, speculative complexity |
| Q43 | Scenario: design a parking lot system | Applying all principles together in one design |
| Q44 | Common OOP interview mistakes | Confusing pillars, no trade-off discussion |
| Q45 | Language-specific OOP quirks | Java/Python/JS/C# differences across 5 dimensions |
Frequently Asked Questions
What are the most commonly asked OOP interview questions?
The four pillars of OOP (Q4), the difference between encapsulation and abstraction (Q11), method overloading vs overriding (Q24), and abstract class vs interface (Q14) appear in nearly every OOP interview regardless of language or seniority level. At mid-level and above, interviewers also consistently ask about SOLID principles (Q28-Q33) and at least one design pattern, most commonly Singleton (Q35) or Factory (Q36).
Is OOP still relevant in 2026 compared to functional programming?
Yes. OOP and functional programming are not mutually exclusive, and most modern languages (Java, Python, JavaScript, C#, Kotlin, Swift) support both paradigms. The vast majority of enterprise codebases, frameworks (Spring, NestJS, Angular, Django), and system designs are still built on OOP foundations. Functional concepts (immutability, pure functions, higher-order functions) are increasingly adopted within OOP codebases, but they complement OOP rather than replace it.
Interview preparation should cover OOP thoroughly because it remains the dominant paradigm in job interviews, especially for backend, full-stack, and system design roles.
How should I prepare for OOP interview questions?
Start with the four pillars (Q4) and make sure you can explain each with a concrete code example, not just a definition. Then work through the SOLID principles (Q28-Q33), focusing on the violation-and-fix format that interviewers expect. Practice the comparison questions (encapsulation vs abstraction, overloading vs overriding, abstract class vs interface) because these are the most commonly asked and the most commonly answered incorrectly.
- Master the four pillars with code examples in your primary language
- Practice SOLID principles using the violation-and-fix format
- Learn 3-4 design patterns deeply (Singleton, Factory, Observer, Strategy) rather than all 23 superficially
- Prepare for at least one scenario question (parking lot, library system, vending machine)
- Know the cross-language differences if the role uses multiple languages (Q45)
What OOP topics are specifically asked in senior-level interviews?
Senior interviews shift from "what is X" definitions to "when would you use X and what are the trade-offs" judgment questions. Expect SOLID principles with real-world violation examples (Q29-Q33), the composition over inheritance discussion (Q18), design pattern selection ("which pattern fits this scenario"), and the "when NOT to use a pattern" question (Q42). Scenario-based design questions (Q43) where you apply multiple principles together are the hallmark of senior OOP interviews.
For senior backend roles, combine this guide with our SQL interview questions and NestJS interview questions guides for comprehensive preparation.
Which programming language should I use to answer OOP interview questions?
Use whichever language you are most comfortable with, unless the job posting specifies a particular language. Java and Python are the most common choices: Java because its OOP implementation is strict and explicit (access modifiers are enforced, interfaces and abstract classes are distinct language features), and Python because its syntax is clean and quick to write on a whiteboard or in a shared editor.
If the role involves multiple languages, mentioning cross-language differences (Q45) unprompted demonstrates breadth. For example, noting that Python's access modifiers are convention-based rather than enforced, or that JavaScript uses prototype-based inheritance under the hood, signals genuine understanding beyond one ecosystem.
What is the easiest way to remember the difference between encapsulation and abstraction?
Encapsulation hides DATA (using access modifiers like private/protected). Abstraction hides COMPLEXITY (using interfaces and abstract classes). Encapsulation answers "how do I protect internal state?" Abstraction answers "what do I expose to the user?"
A concrete memory aid: a BankAccount class with a private double balance field is encapsulation (data hiding). A PaymentProcessor interface with a processPayment() method is abstraction (complexity hiding). See Q11 for the full comparison with code examples.
Related Articles
30 NestJS Interview Questions and Answers (2026)
30 NestJS interview questions with full answers: modules, DI, guards, pipes, interceptors, JWT auth, microservices, and testing. Updated for 2026.
30 Node.js Interview Questions and Answers (2026)
30 Node.js interview questions with full answers: event loop, streams, clustering, worker threads, memory leaks, and security. Updated for 2026.
40 SQL Interview Questions and Answers (2026)
40 SQL and relational database interview questions covering joins, indexes, ACID, window functions, CTEs, MySQL vs PostgreSQL, and query challenges.