The 60 OOPs questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
Object-Oriented Programming (OOPs) is a way of structuring software around objects: self-contained units that hold data and the behavior that operates on that data. Instead of a long list of functions passing values around, you group related state and methods into a class, then create objects (instances) from it. Oracle's official Java documentation frames OOPs through four concepts that show up in nearly every interview: encapsulation (hiding internal state behind a public interface), abstraction (exposing what an object does, not how), inheritance (a class reusing and extending another), and polymorphism (one interface, many behaviors). In interviews, questions probe whether you understand these ideas well enough to apply them, when a subclass is the right tool versus composition, why you'd make a field private, how method overriding differs from overloading. This page collects the 60 questions that come up most, each with a direct answer and runnable code. If a live coding round is next, pair this bank with our AI coding interview prep for the format side.
Watch: Python Object Oriented Programming (OOP): For Beginners
Video: Python Object Oriented Programming (OOP): For Beginners (Tech With Tim, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your OOPs certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
OOPs is a way of structuring code around objects that bundle data with the methods that act on it. Instead of scattered functions passing values around, you group related state and behavior into classes and create objects from them.
The goal is code that maps to the real world: a BankAccount object holds a balance and knows how to deposit and withdraw. This keeps related things together, hides internal detail, and makes large systems easier to reason about.
Key point: Open with a one-line definition, then The four pillars. Interviewers use this question to hear whether you can structure an answer.
Watch a deeper explanation
Video: Intro to Object Oriented Programming: Crash Course (freeCodeCamp.org, YouTube)
Encapsulation, abstraction, inheritance, and polymorphism. Encapsulation bundles data with methods and hides internal state. Abstraction exposes what an object does, not how. Inheritance lets a class reuse and extend another. Polymorphism lets one interface drive many behaviors.
Being able to define each and give a real example is the bar. Most OOPs rounds open here and branch out from your answer.
| Pillar | What it does | Everyday example |
|---|---|---|
| Encapsulation | Hides state behind an interface | A car's engine, controlled by a pedal |
| Abstraction | Shows what, hides how | Driving without knowing combustion detail |
| Inheritance | Reuse and extend a class | An ElectricCar is a Car |
| Polymorphism | One interface, many behaviors | start() works for any Vehicle |
Key point: Pair each pillar with a concrete example. Definitions alone read as memorized; examples read as understood.
A class is a blueprint that defines structure (fields) and behavior (methods). An object is a concrete instance created from that class, with its own copy of the state.
One Car class can produce many Car objects, each with a different color and speed. The class is the design; objects are the things you actually work with at runtime.
class Car {
String color;
Car(String color) { this.color = color; }
}
Car red = new Car("red"); // one object
Car blue = new Car("blue"); // another object, same classKey point: The follow-up is usually 'can you have a class with no objects?' (yes, abstract or static-only). Have that ready.
Encapsulation bundles data and the methods that operate on it inside one unit, and hides the internal state behind a public interface. Callers change state through methods, not by touching fields directly.
You do it by making fields private and exposing controlled getters and setters. That lets you validate input, keep invariants, and change the internals later without breaking callers.
class Account {
private double balance; // hidden state
public void deposit(double amount) {
if (amount > 0) balance += amount; // validated
}
public double getBalance() { return balance; }
}Key point: The point isn't getters and setters, it's control. Say why hiding state matters: validation and freedom to change internals.
Watch a deeper explanation
Video: Object Oriented Programming: The Four Pillars of OOP (Keep On Coding, YouTube)
Abstraction exposes what an object does while hiding how it does it. You drive a car with a pedal and steering wheel without knowing the engine internals. In code, abstract classes and interfaces define a contract and leave the implementation to subclasses.
The distinction from encapsulation: abstraction is about design (what to expose at the interface), encapsulation is about implementation (hiding and protecting state). They work together but answer different questions.
| Abstraction | Encapsulation | |
|---|---|---|
| Focus | What an object does | How state is protected |
| Level | Design and interface | Implementation detail |
| Achieved by | Interfaces, abstract classes | Access modifiers (private) |
Key point: This pair trips up freshers. A crisp 'abstraction is design, encapsulation is implementation' signals real understanding.
Inheritance lets a class (the child or subclass) reuse and extend another class (the parent or superclass). The child gets the parent's fields and methods and can add its own or override existing ones.
It models an is-a relationship: a Dog is an Animal, so Dog extends Animal and inherits eat() while adding bark(). The payoff is code reuse; the risk is tight coupling to the parent, which is why deep hierarchies are discouraged.
class Animal {
void eat() { System.out.println("eating"); }
}
class Dog extends Animal {
void bark() { System.out.println("woof"); }
}
Dog d = new Dog();
d.eat(); // inherited
d.bark(); // own methodKey point: Say 'is-a' out loud. The natural follow-up is composition (has-a) and when to prefer it.
Polymorphism means one interface driving many behaviors. The same method call resolves to different implementations depending on the actual object's type. A draw() call on a Shape reference runs Circle.draw() or Square.draw() based on what the object really is.
It comes in two forms: compile-time (method overloading, same name different parameters) and runtime (method overriding, a subclass replacing a parent method). Runtime polymorphism is what makes flexible, extensible designs possible.
class Shape { void draw() { System.out.println("shape"); } }
class Circle extends Shape { void draw() { System.out.println("circle"); } }
Shape s = new Circle();
s.draw(); // "circle" - resolved at runtimeKey point: Distinguish overloading from overriding when you answer. Interviewers often ask for both forms in the same breath.
Overloading is two or more methods with the same name but different parameter lists in the same class, resolved at compile time. Overriding is a subclass providing its own version of a method it inherited, with the same signature, resolved at runtime.
Overloading is a convenience (one name, several argument shapes). Overriding is the mechanism behind runtime polymorphism.
// Overloading: same name, different params
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
// Overriding: subclass replaces parent method
class Base { String name() { return "base"; } }
class Sub extends Base { String name() { return "sub"; } }| Overloading | Overriding | |
|---|---|---|
| Where | Same class | Parent and subclass |
| Signature | Different parameters | Same signature |
| Resolved | Compile time | Runtime |
| Purpose | Convenience | Runtime polymorphism |
Key point: A common trap: candidates say return type distinguishes overloads. It doesn't; only the parameter list does.
A constructor is a special method that runs when an object is created, setting up its initial state. It shares the class name and returns no value, not even void.
If you write none, most languages provide a default no-argument constructor. Once you add any constructor, that free default disappears unless you write it yourself.
class Point {
int x, y;
Point(int x, int y) { // constructor
this.x = x;
this.y = y;
}
}
Point p = new Point(3, 5);Key point: Expect 'what is a copy constructor?' and 'what happens if you don't write one?' as follow-ups.
Three common kinds: a default (no-argument) constructor that sets fields to defaults, a parameterized constructor that takes arguments to initialize state, and a copy constructor that builds a new object from an existing one.
Not every language names all three (Java has no built-in copy constructor; you write one), but the concepts carry across.
class Box {
public:
int w;
Box() { w = 0; } // default
Box(int width) { w = width; } // parameterized
Box(const Box& other) { w = other.w; } // copy
};A destructor is a method that runs when an object is destroyed, used to release resources it held, such as memory, file handles, or connections. In C++ it's ~ClassName() and runs deterministically when the object goes out of scope.
Languages with garbage collection (Java, Python) don't use classic destructors the same way. Java has no reliable destructor; Python's __del__ exists but timing isn't guaranteed, so both prefer explicit cleanup like try-with-resources or context managers.
class File {
public:
File() { /* open */ }
~File() { /* close */ } // runs at scope exit
};Access modifiers control who can see a class member. The common set is public (accessible everywhere), private (only inside the class), and protected (the class plus its subclasses). Java adds a package-private default when you write no modifier.
They're the tool that makes encapsulation real: mark state private, expose behavior public, and callers can't reach past your interface.
| Modifier | Same class | Subclass | Anywhere |
|---|---|---|---|
| public | Yes | Yes | Yes |
| protected | Yes | Yes | No |
| private | Yes | No | No |
this refers to the current object, the instance whose method is running. It disambiguates a field from a parameter of the same name (this.x = x), passes the current object to another method, or chains constructors.
In Python the same role is filled by an explicit self parameter, which is why Python methods list self first.
class Timer {
int seconds;
Timer(int seconds) {
this.seconds = seconds; // field = parameter
}
}A static member belongs to the class itself, not to any single object. One copy is shared across all instances, and you access it through the class name. A static counter shared by every object, or a utility method that needs no instance state, are the usual cases.
A static method can't use this or instance fields, because there's no particular object to refer to. That restriction is the whole point.
class Counter {
static int total = 0; // shared by all objects
Counter() { total++; }
}
new Counter();
new Counter();
System.out.println(Counter.total); // 2An abstract class is a class you can't instantiate directly. It can hold both concrete methods (shared implementation) and abstract methods (no body) that subclasses must implement. It exists to be extended.
Use one when several classes share behavior and state but also need their own version of some methods. A Shape base class with a concrete describe() and an abstract area() is the classic example.
abstract class Shape {
abstract double area(); // subclasses fill in
void describe() { // shared
System.out.println("area=" + area());
}
}
class Circle extends Shape {
double r;
Circle(double r) { this.r = r; }
double area() { return Math.PI * r * r; }
}An interface is a contract: a set of method signatures a class promises to implement, with no state and (traditionally) no implementation. It says what a type can do without saying how.
A class can implement many interfaces, which is how languages like Java get the flexibility of multiple inheritance without its ambiguity. Interfaces are the backbone of loose coupling: code depends on the contract, not the concrete class.
interface Payable {
void pay(double amount);
}
class Invoice implements Payable {
public void pay(double amount) {
System.out.println("paid " + amount);
}
}Key point: The classic follow-up is 'interface vs abstract class'. It's covered in the intermediate tier; know the difference cold.
An is-a relationship is inheritance: a Dog is an Animal, so Dog extends Animal. A has-a relationship is composition: a Car has an Engine, so Car holds an Engine object as a field.
The rule of thumb: reach for inheritance only when the subclass truly is a specialized parent; otherwise compose. Misusing inheritance for code reuse is one of the most common design mistakes.
// is-a: inheritance
class ElectricCar extends Car { }
// has-a: composition
class Car {
Engine engine = new Engine();
}Getters and setters are methods that read and write a private field. They're the controlled doorway to encapsulated state: a setter can validate input, a getter can compute or format on the way out.
The real reason to use them isn't ceremony, it's future-proofing. a plain field, and if validation or logging is needed later, add it inside the accessor without changing a single caller comes first.
class User {
private int age;
public int getAge() { return age; }
public void setAge(int age) {
if (age >= 0) this.age = age; // validation
}
}Single (one child, one parent), multilevel (a chain: C extends B extends A), hierarchical (many children share one parent), and multiple (one child, several parents). Multiple inheritance of classes is where languages diverge: C++ allows it, Java forbids it for classes but allows multiple interfaces.
Knowing which your target language supports matters, because the ambiguity from multiple class inheritance (the diamond problem) is a common follow-up.
super refers to the parent class. It calls the parent's constructor (super(args)) or invokes a parent method that the subclass has overridden (super.method()), so you can extend behavior instead of fully replacing it.
Calling super() in a constructor is how a subclass makes sure the parent's initialization runs before its own.
class Animal {
Animal() { System.out.println("animal built"); }
}
class Dog extends Animal {
Dog() {
super(); // run parent constructor first
System.out.println("dog built");
}
}For candidates with working experience: design judgment, standard trade-offs, and the questions that separate users from designers.
An abstract class can hold state (fields) and concrete methods that subclasses inherit; a class extends only one of them. An interface is a pure contract of method signatures with no state, and a class can implement many.
Choose an abstract class when subclasses share code and state and form an is-a family. Choose an interface when unrelated classes need to promise the same capability. Modern Java blurs this a little with default methods on interfaces, worth mentioning as a nuance.
| Abstract class | Interface | |
|---|---|---|
| State (fields) | Yes | Constants only (traditionally) |
| Method bodies | Yes | Only defaults (modern) |
| How many | Extend one | Implement many |
| Best for | Shared code in an is-a family | A capability across unrelated types |
Key point: The strongest answer ends with a decision rule, not just a feature table: shared implementation to abstract class, capability contract to interface.
Watch a deeper explanation
Video: Object-Oriented Programming, Simplified (Programming with Mosh, YouTube)
Inheritance couples a subclass tightly to its parent's implementation: a change in the base can quietly break every child, and deep hierarchies become rigid. Composition assembles behavior from smaller parts a class holds, so you can swap and combine capabilities without a fragile family tree.
The guideline 'favor composition over inheritance' doesn't ban inheritance; it says reach for it only when there's a true is-a relationship, and prefer has-a assembly the rest of the time.
// Inheritance couples Duck to a fixed fly() in the base
// Composition injects the behavior instead:
class Duck {
private FlyBehavior fly;
Duck(FlyBehavior fly) { this.fly = fly; }
void performFly() { fly.execute(); }
}
// Now a rubber duck just gets a NoFly behavior, no override needed.Key point: The failure mode: a change in the parent breaking subclasses. That concrete pain is what the key point is.
Watch a deeper explanation
Video: The Flaws of Inheritance (CodeAesthetic, YouTube)
Compile-time (static) polymorphism is method overloading: the compiler picks the method from the argument types before the program runs. Runtime (dynamic) polymorphism is method overriding: the actual object's type decides which method runs, resolved during execution through a mechanism like a virtual method table.
Runtime polymorphism is the one that gives OOPs its flexibility, because you can add new subclasses and existing code calls the right method without changing.
| Compile-time | Runtime | |
|---|---|---|
| Mechanism | Overloading | Overriding |
| Resolved | At compile time | At execution |
| Based on | Argument types | Actual object type |
| Also called | Static / early binding | Dynamic / late binding |
The diamond problem happens when a class inherits from two classes that share a common ancestor. If both middle classes override a method, the bottom class faces ambiguity about which version it inherits, and the shared ancestor's state can be duplicated.
Languages solve it differently: C++ uses virtual inheritance to share one copy of the ancestor, Java sidesteps it by forbidding multiple class inheritance (interfaces don't carry state, so no ambiguity), and Python resolves method order with a defined MRO.
class A:
def who(self): return 'A'
class B(A):
def who(self): return 'B'
class C(A):
def who(self): return 'C'
class D(B, C): pass
print(D().who()) # 'B', decided by the MRO
print([c.__name__ for c in D.__mro__]) # D, B, C, A, objectA virtual function is one whose call resolves to the object's actual type at runtime, not the reference type. In C++ you mark it virtual; in Java methods are virtual by default. This is what makes overriding work: a Shape reference pointing to a Circle calls Circle.draw().
Under the hood, each class with virtual methods has a table of function pointers (a vtable), and each object carries a pointer to its class's table. The call looks up the right function through that table, which is why it's called dynamic dispatch.
class Shape {
public:
virtual void draw() { cout << "shape"; }
};
class Circle : public Shape {
public:
void draw() override { cout << "circle"; }
};
Shape* s = new Circle();
s->draw(); // "circle" via the vtableFive design guidelines for maintainable OOPs code. Single Responsibility: a class should have one reason to change. Open/Closed: open for extension, closed for modification. Liskov Substitution: a subclass must be usable wherever its parent is. Interface Segregation: prefer small focused interfaces over fat ones. Dependency Inversion: depend on abstractions, not concrete classes.
Interviewers rarely want all five recited; they want you to spot a violation and fix it. Being able to The principle a piece of bad code breaks is the real test.
Key point: Have one concrete violation-and-fix ready for at least two letters. Recitation scores low; applied judgment scores high.
Take a payment processor. Abstraction is the interface: a pay(amount) method that callers use without knowing whether it's Stripe or PayPal underneath. That's the design decision about what to expose.
Encapsulation is inside the implementation: the Stripe class keeps its API key and retry counter private and validates the amount before charging. That's protecting and hiding the state. Same class, two different ideas working together.
Binding is matching a method call to its definition. Static (early) binding happens at compile time, used for private, static, final, and overloaded methods where the target is known without running the program. Dynamic (late) binding happens at runtime for overridden methods, where the object's real type decides.
The distinction explains a common gotcha: a variable's declared type drives overload resolution (static), but its actual object drives override resolution (dynamic).
Coupling measures how much one class depends on another; cohesion measures how focused a single class is on one job. Good design aims for low coupling (classes change independently) and high cohesion (each class does one thing well).
High coupling means a change ripples across many classes. Low cohesion means a class does too many unrelated things and is hard to name. You reduce coupling with interfaces and dependency injection, and raise cohesion by splitting bloated classes.
| Coupling | Cohesion | |
|---|---|---|
| Measures | Dependence between classes | Focus within a class |
| You want | Low | High |
| Improved by | Interfaces, dependency injection | Single responsibility |
All three describe how objects relate. Association is a general link (a Teacher uses a Classroom). Aggregation is a weak has-a where the part can outlive the whole (a Team has Players, but players exist without the team). Composition is a strong has-a where the part's lifetime is tied to the whole (a House has Rooms; destroy the house and the rooms go with it).
The lifetime question is the deciding line: does the part survive without the whole?
| Relationship | Strength | Part outlives whole? |
|---|---|---|
| Association | Loose link | Yes |
| Aggregation | Weak has-a | Yes |
| Composition | Strong has-a | No |
Method hiding happens with static methods: a subclass declares a static method with the same signature as the parent's, and which one runs depends on the reference type, not the object. Overriding is for instance methods and depends on the object's actual type.
The practical warning: hiding looks like overriding but behaves differently, so calling a hidden static method through a parent reference runs the parent's version. It's a frequent trick question.
class Base {
static String label() { return "base"; }
}
class Sub extends Base {
static String label() { return "sub"; } // hides, not overrides
}
Base b = new Sub();
System.out.println(b.label()); // "base" - reference type winsfinal locks something down. A final field can't be reassigned after initialization, a final method can't be overridden, and a final class can't be extended. It communicates intent and lets the compiler optimize.
You'd make a class final to prevent subclassing that could break invariants (String is final in Java), and a method final to stop subclasses from changing behavior the base relies on.
Constructor chaining is one constructor calling another so initialization logic lives in one place. Within a class you chain with this(args); to a parent you chain with super(args). A parameterized constructor can delegate to a fuller one that does the real setup.
It avoids duplicating field assignments across overloaded constructors. The chain always runs the parent constructor first, then works down.
class Box {
int w, h;
Box() { this(1, 1); } // chains to below
Box(int w, int h) {
this.w = w;
this.h = h;
}
}A shallow copy duplicates the object's top-level fields but shares any referenced sub-objects: copying an Order copies the reference to its Customer, so both orders point to the same customer. A deep copy recursively duplicates the referenced objects too, giving a fully independent tree.
The classic bug is mutating a nested object through what you assumed was an independent copy. Deep copy when the object owns its sub-objects; shallow copy when sharing is fine.
import copy
class Order:
def __init__(self, items):
self.items = items
o1 = Order([1, 2])
shallow = copy.copy(o1)
shallow.items.append(3)
print(o1.items) # [1, 2, 3] - shared list
deep = copy.deepcopy(o1)
deep.items.append(9)
print(o1.items) # [1, 2, 3] - independentDependency injection means a class receives its collaborators from outside (through the constructor or a setter) instead of creating them itself. A ReportService is handed a Database rather than doing new Database() internally.
It's the practical form of the Dependency Inversion principle: the class depends on an abstraction it's given, which makes it easy to swap a real database for a fake in tests and cuts coupling.
class ReportService {
private final Database db;
ReportService(Database db) { // injected, not created
this.db = db;
}
}
// tests pass a fake Database; production passes the real oneAn immutable object can't change after construction: all fields are set once and never reassigned. String in Java and tuples in Python are immutable. You build one by making fields final/private, providing no setters, and returning copies of any mutable internals.
The payoff is safety: immutable objects are inherently thread-safe, can be shared freely without defensive copying, and make great map keys because their hash never changes.
final class Money {
private final int cents;
Money(int cents) { this.cents = cents; }
int getCents() { return cents; }
Money add(Money other) { // returns a new object
return new Money(this.cents + other.cents);
}
}It lets you write code against a general type and add new specific types without changing that code. A renderer that loops over a list of Shape and calls draw() works for Circle, Square, and any future shape, no if-else on type needed.
That's the open/closed principle in action: the drawing loop is closed for modification but open to new shapes. Without polymorphism you'd have a growing switch statement that every new type forces you to edit.
for (Shape s : shapes) {
s.draw(); // right method per object, no type checks
}Cloning creates a copy of an existing object. Java's Cloneable and clone() give a shallow copy by default, which shares mutable sub-objects and surprises people. Many teams avoid clone() entirely in favor of copy constructors or factory methods that make the copy semantics explicit.
The pitfalls: shallow copies sharing nested state, clone() bypassing constructors so invariants can be skipped, and inconsistent behavior across a class hierarchy. Being aware of these is the intermediate-level signal.
Upcasting treats a subclass object as its parent type (Animal a = new Dog()); it's always safe and implicit. Downcasting goes the other way (Dog d = (Dog) a), treating a parent reference as a subclass, and it's only safe if the object really is that subclass, otherwise you get a runtime error.
Before downcasting, check with instanceof. Frequent downcasting is often a smell that polymorphism should be doing the work instead.
Animal a = new Dog(); // upcast, safe
if (a instanceof Dog) {
Dog d = (Dog) a; // downcast, checked
d.bark();
}The overriding method needs the same name and parameter list, a return type that's the same or a subtype (covariant return), and access that's the same or more permissive, not narrower. It can't throw broader checked exceptions than the parent declares.
Break any of these and it's no longer an override: it becomes an overload, a compile error, or silent method hiding. The @Override annotation exists to catch these mistakes at compile time.
Key point: Mention @Override. Using it shows you've been bitten by a typo silently creating an overload instead of an override.
advanced rounds probe design judgment, trade-offs, and production scars. Expect every answer here to draw a follow-up.
Design patterns are reusable solutions to common design problems, named so teams can discuss them quickly. The classic groups are creational (Factory, Singleton, Builder), structural (Adapter, Decorator, Facade), and behavioral (Observer, Strategy, Command).
The production signal isn't reciting the catalog; it's naming a pattern you actually used and why. Strategy to swap algorithms at runtime, Observer for event notification, Factory to decouple creation from use. Pair each with a real problem it solved.
Key point: Have two patterns you've genuinely used with the problem they solved. Reciting the GoF list without application indicates textbook, not experience.
A singleton guarantees one instance and a global access point: a private constructor, a static holder, and a static accessor. In a threaded context you need lazy initialization done safely, and the initialization-on-demand holder idiom (or an enum in Java) handles that cleanly.
The downsides are real: singletons act as global state, hide dependencies, and make testing hard because you can't easily substitute them. Many teams treat overuse of singletons as a design smell and prefer dependency injection with a single configured instance.
class Config {
private Config() {}
private static class Holder {
static final Config INSTANCE = new Config();
}
static Config get() { return Holder.INSTANCE; } // thread-safe, lazy
}Key point: Volunteering the downsides is the move. Anyone can write a singleton; seniors know why to avoid reaching for it.
A Factory decides which class to instantiate and hides that choice from the caller: you ask for a Shape and get back a Circle or Square based on input. A Builder assembles one complex object step by step, useful when a constructor would have many parameters or optional fields.
Rule of thumb: Factory for choosing a type, Builder for constructing a single object with many configuration options. They solve different problems and sometimes appear together.
| Factory | Builder | |
|---|---|---|
| Solves | Which type to create | How to assemble one object |
| Caller sees | An abstract type back | A fluent step-by-step API |
| Good when | Type varies by input | Many optional parameters |
The classic one is Square extends Rectangle. A Rectangle lets you set width and height independently; a Square forces them equal. Code that sets a Rectangle's width to 5 and height to 4 expecting area 20 breaks when handed a Square, which silently changes both dimensions.
The fix is to not model it as inheritance: Square and Rectangle share a Shape interface with area(), rather than Square pretending to be a substitutable Rectangle. LSP violations show up as subclasses that surprise code written for the parent.
// Violates LSP: a Square breaks Rectangle's contract
Rectangle r = new Square();
r.setWidth(5);
r.setHeight(4);
// caller expects area 20; Square makes it 16You design so new behavior is added by writing new classes, not editing existing ones. Instead of a discount() method with a growing switch over customer types, define a DiscountStrategy interface and add a new strategy class per rule. The pricing code never changes when a new discount appears.
The trade-off worth naming: over-abstracting for changes that never come adds complexity. Apply Open/Closed where variation is real and expected, not everywhere on principle.
interface DiscountStrategy { double apply(double price); }
// add StudentDiscount, SeasonalDiscount as new classes
// the pricing engine loops over strategies and never changesSubclassing to vary one behavior explodes the class count and locks the choice in at compile time: a SortAscending and SortDescending subclass, then variants for every combination. Strategy pulls the varying behavior into its own interface, injected as an object, so you swap it at runtime and combine freely.
It's composition over inheritance applied to algorithms. The object holds a strategy field instead of being a specific subclass, which keeps the hierarchy flat and the behavior interchangeable.
Refactoring a switch to Strategy
The payoff: adding a new behavior means writing a new class, not editing the context. That's Open/Closed made concrete.
Circular dependencies (A needs B, B needs A) signal that responsibilities are tangled. The usual fixes: extract the shared logic into a third class both depend on, invert one direction with an interface so A depends on an abstraction B implements, or merge the two if they're really one concept.
In frameworks with dependency injection, circular constructor dependencies fail at startup, which is a useful forcing function. Setter injection or a mediator can break the cycle, but the cleaner design is usually to rethink the boundaries.
A God object is a class that knows and does too much: hundreds of methods, many unrelated responsibilities, touched by every change. It violates single responsibility and becomes a merge-conflict magnet and a testing nightmare.
You refactor by identifying cohesive clusters of methods and fields, then extracting each into its own focused class, wiring them back through composition. Do it incrementally with tests as a safety net, because a big-bang rewrite of a central class is risky.
Default methods let an interface ship a method body, so you can add a method to a widely-implemented interface without breaking every implementer. That's how Java evolved the Collection interfaces without a mass migration.
They complicate the abstract-class-versus-interface line and reintroduce a mild multiple-inheritance ambiguity: if two interfaces provide the same default method, the implementing class must resolve it explicitly. Used sparingly for evolution they're fine; used to stuff logic into interfaces they blur design.
When applied dogmatically to code that doesn't need it. Splitting every class to satisfy single responsibility can scatter simple logic across ten files. Adding interfaces for dependency inversion where there's only ever one implementation is indirection with no payoff.
The senior view treats SOLID as a response to actual change and pain, not a checklist. You reach for an interface when a second implementation is real or imminent, not preemptively. Naming that trade-off is what separates experience from principle-recitation.
Key point: This question is a trap for dogmatists. Show you apply principles to solve real problems, not to win an argument against a checklist.
Loose coupling and dependency injection make code testable: if a class receives its collaborators, you pass fakes in tests. Tight coupling, static calls, and singletons make it hard because you can't substitute behavior. Small, single-responsibility classes are easier to test than God objects.
So testability is a design feedback signal. If a class is painful to test, that pain usually points at a coupling or responsibility problem worth fixing, not just a missing mock.
A leaky abstraction is one whose underlying detail forces itself on callers despite the interface trying to hide it. An ORM abstracts SQL until a performance problem makes you reason about the exact queries it generates; the SQL leaks through.
You can't eliminate leaks entirely (all non-trivial abstractions leak somewhat), so you manage them: document the leak, provide an escape hatch (a way to drop to raw queries), and don't pretend the abstraction is total. Pretending it's leak-free is where teams get burned.
With value semantics, assigning or passing an object copies it, so changes don't propagate. With reference semantics, you share a handle to one object, so a mutation is visible everywhere it's referenced. Most OOPs languages use reference semantics for objects, which is why aliasing bugs happen.
It matters for equality (== on references compares identity, not content, so you override equals/hashCode), for defensive copying at boundaries, and for the case for immutable objects, which sidestep the whole aliasing problem.
List<Integer> a = new ArrayList<>(List.of(1, 2));
List<Integer> b = a; // reference, not a copy
b.add(3);
System.out.println(a); // [1, 2, 3] - same objectKeep it shallow. Each level of inheritance couples subclasses to more ancestor behavior, and deep trees become rigid and hard to trace. A good target is one or two levels, with composition doing the rest of the work.
The heuristic: inherit only for true is-a relationships where the subclass is genuinely substitutable, and where the shared behavior is stable. If you're inheriting mainly to reuse code, that's a signal to compose instead. Depth for its own sake is a smell.
By returning a reference to a mutable internal object. A getItems() that hands back the actual internal list lets callers mutate your object's state directly, bypassing every validation and invariant the class tried to protect.
The fix is to return a copy or an unmodifiable view. This is the difference between exposing data and exposing a controlled read, and it's a frequent source of subtle bugs in otherwise well-encapsulated classes.
class Cart {
private final List<Item> items = new ArrayList<>();
// leaky: caller can mutate internal state
List<Item> getItemsBad() { return items; }
// safe: hand back a read-only view
List<Item> getItems() { return List.copyOf(items); }
}Replace a type-based switch or if-else chain with polymorphism when the same branching logic appears in several places and new types are added often; each new type then means one new class instead of edits scattered across the codebase.
Don't do it for a one-off conditional, simple value comparisons, or logic that isn't type-driven. Turning every if into a class hierarchy is over-engineering that hurts readability. The trigger is repeated type-switching, not conditionals in general.
Deep inheritance couples applications to framework internals: a base class you must extend dictates your structure and breaks you on framework upgrades. Composition and interfaces let the framework offer capabilities you plug in, so your code depends on small contracts, not a fragile parent.
This is why patterns shifted from extend-our-base-class toward implement-this-interface and annotation-driven wiring. It keeps the framework's evolution and your code loosely joined, which matters a lot across version upgrades.
Mixins (Python, Ruby) and traits (Scala, Rust) let you compose reusable behavior into a class without classic single-parent inheritance. A class can pull in several small units of behavior, giving code reuse across unrelated hierarchies.
They solve the reuse problem that single inheritance can't: sharing a capability like serialization or comparison across classes that don't share a parent. The risk is method-resolution complexity when several mixins define the same name, so languages define a clear resolution order.
class JsonMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)
class User(JsonMixin): # pulls in behavior, no is-a
def __init__(self, name):
self.name = name
print(User('Asha').to_json()) # {"name": "Asha"}Decide extension points on purpose: mark the methods meant to be overridden, make everything else final or private, and document the contract each overridable method must honor. Never call an overridable method from a constructor, because the subclass override runs before the subclass is fully initialized.
The principle behind it: design and document for inheritance, or prohibit it. A class that's accidentally extensible, with no thought to which methods are safe to override, breaks in surprising ways when someone subclasses it.
Key point: Mentioning the constructor-calling-overridable-method trap is a strong production signal; it bites real codebases and few candidates raise it.
Start from the nouns and their behavior, not a class diagram. A ParkingLot holds Levels; each Level has ParkingSpots of types (compact, large, handicapped); a Vehicle (with subtypes) needs a spot that fits; a Ticket tracks entry time; a pricing strategy computes the fee. Encapsulate spot availability inside Level so nothing else mutates it directly.
Then The design choices out loud: composition for lot-has-levels, an interface for pricing so it's swappable, polymorphism so any Vehicle subtype fits the parking logic. The interviewer is scoring your process, clarify requirements, find entities, assign responsibilities, choose relationships, more than the exact classes.
Key point: Talk through requirements and responsibilities before writing classes; jumping straight to code without clarifying scope loses points.
OOPs and procedural programming solve the same problems with different structure. Procedural code organizes logic into functions that pass data around; OOPs binds data and behavior together inside objects. OOPs shines when a system has many entities with shared behavior and state that must stay consistent, because encapsulation and inheritance cut duplication and keep invariants close to the data. Procedural code stays simpler for short scripts, straight-line data pipelines, and cases where the object ceremony adds overhead without payoff. Knowing which fits a given problem, and saying so out loud, is itself an interview signal.
| Aspect | Procedural | Object-Oriented (OOPs) |
|---|---|---|
| Core unit | Functions and procedures | Objects (data plus methods) |
| Data and behavior | Separate; data passed to functions | Bundled together in objects |
| State protection | Data often global or shared openly | Encapsulated behind an interface |
| Code reuse | Function calls, copy-paste | Inheritance and composition |
| Best fit | Short scripts, linear data flow | Large systems with many related entities |
Prepare in layers, and practice out loud. Most OOPs rounds move from the four pillars to design questions to a live coding or debugging discussion, so rehearse each stage rather than only reading answers.
The typical OOPs interview flow
Earlier rounds increasingly run as AI coding interviews. The pillars and design judgment on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These OOPs questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works