Go2X
Go2X

India's leading training and placement platform offering hands-on learning, powered by 200+ IITian and industry experts, connecting students to 1,000+ hiring and referral partners.

Let's Go2X

Stay updated with Go2X

Get course updates, interview tips, and career insights delivered to your inbox.

Contact Us

Address

1st Floor, Plot No 332, Phase IV, Udyog Vihar,
Sector 19, Gurugram, Haryana 122015

Email

support@go2x.live

Phone

+91 94107 10085

ยฉ 2025 Go2X Private Limited. All rights reserved.

Made with ๐Ÿงก and a lot of late nights.

Interview ExperiencesBlogsAbout UsPrivacy PolicyTerms of ServiceRefund Policy

On This Page:

Other

OOPS Interview Questions

Crack your next OOP interview with 30+ carefully explained questions covering encapsulation, inheritance, polymorphism, SOLID principles, and design patterns - written in plain English with real code examples.

August 09, 2026
32 mins read

I. Beginner Level

1. What is Object-Oriented Programming (OOP)?

Before OOP, most code was written procedurally - just a long list of instructions executing top to bottom. That works fine for small programs. But as programs grew larger, that style turned into a maintenance nightmare. You'd have data scattered everywhere and no clear ownership of who was allowed to change what.

OOP fixes this by organising code around objects. An object bundles together the data it owns (called properties or fields) and the actions it can perform (called methods). Instead of thinking "what steps do I execute?", you think "what things exist in this system, and what can each one do?"

A great real-world analogy: think of a car. A car has data - its colour, fuel level, and current speed. It also has behaviours - you can start it, accelerate, brake, and refuel. In OOP, you'd model a Car class that holds that data and those methods together. Any time you need a car in your program, you create an instance of that class. OOP is built on four core ideas: encapsulation, abstraction, inheritance, and polymorphism. Everything else in OOP flows from these four.

2. What is the difference between a class and an object?

This trips up a lot of beginners, but the distinction is simple once you have the right mental model. A class is a blueprint. An object is something you built using that blueprint.

Think of a class as the architectural plan for a house. The plan describes how many rooms there are, where the doors go, and how big the windows are - but the plan itself is not a house. You can't live in a plan. When a builder follows that plan and actually constructs something, that physical structure is the object - also called an instance.

From one blueprint you can build as many houses as you want. Same thing with classes - you define the class once, then create as many instances (objects) as you need. Each object has its own copy of the data defined in the class.

javascript
1// The blueprint - defined once
2class Car {
3  constructor(brand, colour) {
4    this.brand  = brand;
5    this.colour = colour;
6    this.speed  = 0;
7  }
8
9  accelerate(amount) {
10    this.speed += amount;
11    console.log(`${this.brand} is now going ${this.speed} km/h`);
12  }
13}
14
15// Objects - each one is a separate instance with its own data
16const tesla   = new Car('Tesla',  'white');
17const ferrari = new Car('Ferrari', 'red');
18
19tesla.accelerate(60);   // Tesla is now going 60 km/h
20ferrari.accelerate(120); // Ferrari is now going 120 km/h
21
22// They don't interfere with each other
23console.log(tesla.speed);   // 60
24console.log(ferrari.speed); // 120
25

3. What are the four pillars of OOP?

Every OOP interview will ask you this. Knowing the names is not enough - you need to explain what problem each one solves. Here's the quick version:

PillarOne-line explanationProblem it solves
EncapsulationBundle data and methods together, hide internal detailsPrevents outside code from corrupting an object's internal state
AbstractionExpose only what the user needs, hide implementation complexityReduces complexity - users interact with a simple interface
InheritanceA child class reuses and extends a parent classEliminates code duplication across related classes
PolymorphismSame interface, different behaviour depending on the objectWrite code that works with multiple types without if/else chains

4. What is encapsulation and why does it matter?

Encapsulation is the idea of wrapping an object's data and the methods that operate on that data into a single unit, and then controlling what the outside world can access. It's essentially about drawing a boundary around your object and deciding what goes in and what comes out.

Here's a real-world parallel: your bank account. You can deposit money, withdraw money, and check your balance - but the bank doesn't let you just reach in and change the balance number directly. There's a controlled interface. If you could modify the balance directly, you could set it to a billion dollars with no record of a transaction. Encapsulation prevents that kind of chaos in code.

javascript
1class BankAccount {
2  #balance; // private field - only accessible inside this class
3
4  constructor(initialBalance) {
5    this.#balance = initialBalance;
6  }
7
8  deposit(amount) {
9    if (amount <= 0) throw new Error('Deposit must be positive');
10    this.#balance += amount;
11    console.log(`Deposited โ‚น${amount}. New balance: โ‚น${this.#balance}`);
12  }
13
14  withdraw(amount) {
15    if (amount > this.#balance) throw new Error('Insufficient funds');
16    this.#balance -= amount;
17    console.log(`Withdrew โ‚น${amount}. New balance: โ‚น${this.#balance}`);
18  }
19
20  getBalance() {
21    return this.#balance; // read-only access via a method
22  }
23}
24
25const account = new BankAccount(5000);
26account.deposit(1000);    // Deposited โ‚น1000. New balance: โ‚น6000
27account.withdraw(2000);   // Withdrew โ‚น2000. New balance: โ‚น4000
28
29// โœ— This would throw an error - #balance is private
30// account.#balance = 1000000;
31

Why does this matter in practice? Because your class can change its internal implementation without breaking the code that uses it. The outside world only depends on the public interface - not on the internals. That's what makes refactoring possible without everything falling apart.

5. What is abstraction in OOP?

Abstraction means hiding the complicated stuff and only showing what the user actually needs to see. Think about how you drive a car. You turn the key, press the accelerator, and steer - you have no idea what's happening with the fuel injection, the combustion cycle, or the transmission. That complexity is hidden from you on purpose. The car exposes a simple interface (steering wheel, pedals, gear shift), and you use that without needing to understand the engine.

In OOP, abstraction is achieved through abstract classes and interfaces. An abstract class defines what a group of related classes should be able to do, without specifying how each one should do it. Each subclass fills in the 'how'.

javascript
1// Abstract concept: a payment method
2// Every payment method must be able to 'pay' - but each works differently
3class PaymentMethod {
4  pay(amount) {
5    throw new Error('pay() must be implemented by subclass');
6  }
7}
8
9class CreditCard extends PaymentMethod {
10  pay(amount) {
11    console.log(`Charged โ‚น${amount} to credit card ending in 4242`);
12    // internally: API call to payment gateway, auth token, CVV check...
13    // the caller doesn't know or care about any of that
14  }
15}
16
17class UPI extends PaymentMethod {
18  pay(amount) {
19    console.log(`Sent โ‚น${amount} via UPI`);
20    // internally: different flow entirely
21  }
22}
23
24// The checkout function only cares that it receives a PaymentMethod
25// It doesn't need to know which specific one
26function checkout(cart, paymentMethod) {
27  const total = cart.getTotal();
28  paymentMethod.pay(total); // works for ANY payment method
29}
30

6. What is inheritance in OOP?

Inheritance lets one class (called the child or subclass) acquire the properties and methods of another class (called the parent or superclass). It's OOP's solution to code duplication. Instead of writing the same logic in five different places, you write it once in a parent class and let children inherit it.

Think of it like actual inheritance in a family. A child inherits their parents' traits but also has their own unique characteristics. A Dog and a Cat are both Animals - they both breathe, eat, and sleep. You wouldn't re-define breathing for every species. You'd define it once in Animal and inherit it.

javascript
1class Animal {
2  constructor(name) {
3    this.name = name;
4  }
5
6  eat() {
7    console.log(`${this.name} is eating.`);
8  }
9
10  sleep() {
11    console.log(`${this.name} is sleeping.`);
12  }
13}
14
15// Dog inherits everything from Animal
16class Dog extends Animal {
17  constructor(name, breed) {
18    super(name); // call parent constructor
19    this.breed = breed;
20  }
21
22  bark() {
23    console.log(`${this.name} says: Woof!`);
24  }
25}
26
27class Cat extends Animal {
28  meow() {
29    console.log(`${this.name} says: Meow!`);
30  }
31}
32
33const dog = new Dog('Bruno', 'Labrador');
34dog.eat();   // Inherited from Animal - Bruno is eating.
35dog.bark();  // Own method - Bruno says: Woof!
36
37const cat = new Cat('Whiskers');
38cat.eat();   // Inherited - Whiskers is eating.
39cat.meow();  // Own method - Whiskers says: Meow!
40

One thing to keep in mind: inheritance models an 'is-a' relationship. A Dog IS AN Animal - that makes sense. If you find yourself writing a class that inherits from another but the relationship feels forced, that's a sign you should probably use composition instead (more on that later).

7. What is polymorphism and what are its types?

Polymorphism means 'many forms'. In OOP, it means the same method name behaves differently depending on which object is calling it. It's what lets you write clean, flexible code that doesn't need to know the exact type of every object it's working with.

Real-world analogy: the word 'open'. You open a door differently from how you open a file, which is different from how you open a bank account. Same word, context-dependent behaviour. In code, same method name, different implementation per class.

There are two main types:

  • Compile-time polymorphism (method overloading): Multiple methods with the same name but different parameters. Resolved at compile time. More common in statically typed languages like Java or C++.

  • Runtime polymorphism (method overriding): A child class provides a different implementation of a method it inherited from its parent. Resolved at runtime. This is the more powerful and commonly used form.

javascript
1class Shape {
2  area() {
3    return 0;
4  }
5
6  describe() {
7    console.log(`This shape has an area of ${this.area()}`);
8  }
9}
10
11class Circle extends Shape {
12  constructor(radius) {
13    super();
14    this.radius = radius;
15  }
16
17  area() {
18    return Math.PI * this.radius ** 2; // overrides parent's area()
19  }
20}
21
22class Rectangle extends Shape {
23  constructor(width, height) {
24    super();
25    this.width  = width;
26    this.height = height;
27  }
28
29  area() {
30    return this.width * this.height; // overrides parent's area()
31  }
32}
33
34// The magic of polymorphism - one loop, multiple types
35const shapes = [new Circle(5), new Rectangle(4, 6), new Circle(3)];
36
37shapes.forEach(shape => shape.describe());
38// This shape has an area of 78.53...
39// This shape has an area of 24
40// This shape has an area of 28.27...
41// We never had to check 'if circle... else if rectangle...'
42

8. What is a constructor and what is it used for?

A constructor is a special method that runs automatically the moment you create a new object from a class. Its job is to set up the object's initial state - assign starting values to properties, establish connections, or do any one-time setup the object needs before it's ready to use.

You don't call the constructor manually. It runs on its own when you write new ClassName(). The arguments you pass to new get received by the constructor. If you don't define a constructor, most languages provide a default empty one.

javascript
1class User {
2  constructor(name, email, role = 'user') {
3    // Runs automatically when: new User('Alice', 'alice@email.com')
4    this.name      = name;
5    this.email     = email;
6    this.role      = role;
7    this.createdAt = new Date();  // set at creation time
8    this.isActive  = true;
9
10    console.log(`User account created for ${this.name}`);
11  }
12
13  greet() {
14    return `Hello, I'm ${this.name} and I joined on ${this.createdAt.toDateString()}`;
15  }
16}
17
18const alice = new User('Alice', 'alice@email.com');
19// Automatically logs: User account created for Alice
20
21const admin = new User('Vishal', 'vishal@email.com', 'admin');
22console.log(admin.role); // 'admin'
23console.log(alice.role); // 'user' - default applied
24

9. What is a destructor?

A destructor is the opposite of a constructor - it runs automatically when an object is about to be destroyed or garbage collected. Its purpose is cleanup: close open file handles, release database connections, free allocated memory, cancel timers, anything that needs to happen before the object disappears.

In languages like C++, destructors are critical because memory management is manual. In garbage-collected languages like Java, Python, and JavaScript, the garbage collector handles memory - but destructors (or their equivalents) are still useful for releasing non-memory resources.

LanguageDestructor equivalentWhen it runs
C++~ClassName()When object goes out of scope or delete is called
Python__del__()When the garbage collector reclaims the object
Javafinalize() (deprecated)Before GC collects the object - not reliable
JavaScriptNo native destructorManual cleanup methods or FinalizationRegistry (ES2021)

10. What are access modifiers in OOP?

Access modifiers control the visibility of a class's properties and methods - essentially, who is allowed to read or change them. They're one of the key tools for implementing encapsulation. Think of them as permission levels: some things are public knowledge, some are internal, and some are family secrets.

ModifierAccessible fromReal-world analogy
publicAnywhere - the class, subclasses, and outside codeYour phone number on a business card - anyone can see it
privateOnly inside the class that defines itYour ATM PIN - only you know it
protectedThe class and its subclasses - not outside codeFamily recipe - shared within the family but not published
default / packageAccessible within the same package (Java)Office notice board - visible to colleagues in the same building
javascript
1class Employee {
2  name;           // public  - anyone can read/write
3  #salary;        // private - only this class can access it
4
5  constructor(name, salary) {
6    this.name   = name;
7    this.#salary = salary;
8  }
9
10  // Controlled access via a public method
11  getSalary() {
12    return this.#salary;
13  }
14
15  // Only the class itself can use this
16  #calculateBonus() {
17    return this.#salary * 0.10;
18  }
19
20  getAnnualPackage() {
21    return (this.#salary * 12) + this.#calculateBonus();
22  }
23}
24
25const emp = new Employee('Riya', 80000);
26console.log(emp.name);           // โœ… 'Riya'
27console.log(emp.getSalary());    // โœ… 80000
28// console.log(emp.#salary);    // โœ— SyntaxError - private!
29

II. Intermediate Level

1. What is the difference between encapsulation and abstraction?

These two get confused constantly - even by experienced developers. The key is to remember they answer different questions. Encapsulation answers: 'how do I protect this data?' Abstraction answers: 'how do I simplify this interface?'

Here's the clearest way to think about it: encapsulation is about data protection. It hides the internal state and makes sure it can only be changed in controlled ways. Abstraction is about complexity hiding. It lets you interact with a system without knowing how it works internally.

FeatureEncapsulationAbstraction
FocusProtecting data from unauthorised accessHiding complexity from the user
Achieved usingAccess modifiers (private, protected)Abstract classes and interfaces
Question it answers'Who can change this data?''What does this thing do (not how)?'
Real-world exampleA pill bottle with a child-proof cap - the medicine is protectedA TV remote - you press buttons without knowing the circuit board

2. What is the difference between method overloading and method overriding?

Both involve a method that shares its name with another method - but that's where the similarity ends. They happen in different contexts, solve different problems, and work differently under the hood.

Method overloading is when a class has multiple methods with the same name but different parameter signatures. It's compile-time polymorphism - the compiler decides which version to call based on the arguments. Overriding is when a child class replaces a method it inherited from its parent with its own version. It's runtime polymorphism - the JVM or runtime decides which version to call based on the actual object type.

FeatureOverloadingOverriding
Where it happensSame classParent and child class
ParametersMust be different (number or type)Must be exactly the same
Resolved atCompile timeRuntime
Return typeCan be differentMust be same (or covariant)
Polymorphism typeCompile-time (static)Runtime (dynamic)
javascript
1// METHOD OVERRIDING (runtime polymorphism) - most relevant in JS
2class Logger {
3  log(message) {
4    console.log(`[LOG] ${message}`);
5  }
6}
7
8class FileLogger extends Logger {
9  log(message) {
10    // Overrides parent's log() with a different implementation
11    console.log(`[FILE] Writing to disk: ${message}`);
12  }
13}
14
15class CloudLogger extends Logger {
16  log(message) {
17    console.log(`[CLOUD] Sending to server: ${message}`);
18  }
19}
20
21// Runtime decides which log() to call based on the actual object
22const loggers = [new Logger(), new FileLogger(), new CloudLogger()];
23loggers.forEach(l => l.log('App started'));
24// [LOG] App started
25// [FILE] Writing to disk: App started
26// [CLOUD] Sending to server: App started
27

3. What is the difference between an abstract class and an interface?

This is one of the most frequently asked OOP questions in technical interviews. The confusion is understandable - both define what subclasses must implement. The difference is in how much they provide.

An abstract class is a partially built class. It can have some methods with implementations (concrete methods) and some methods that are declared but not implemented (abstract methods). It can also hold state (fields). A class can only inherit from one abstract class.

An interface is a pure contract. It says 'you must implement these methods' but provides zero implementation itself. A class can implement multiple interfaces. Think of an interface as a job description - it lists what you must be able to do, not how to do it.

FeatureAbstract ClassInterface
ImplementationCan have both abstract and concrete methodsOnly abstract methods (pure contract)
State (fields)Can have instance variablesCannot have instance variables (only constants)
Multiple inheritanceA class can extend only ONE abstract classA class can implement MULTIPLE interfaces
ConstructorCan have a constructorCannot have a constructor
When to useWhen related classes share common code and stateWhen unrelated classes need to fulfil a common contract
javascript
1// Abstract class - shared code + enforced contract
2class Vehicle {
3  constructor(brand) {
4    if (new.target === Vehicle) throw new Error('Cannot instantiate abstract class');
5    this.brand = brand;
6    this.speed = 0;
7  }
8
9  // Concrete method - shared implementation
10  stop() { this.speed = 0; console.log(`${this.brand} stopped`); }
11
12  // Abstract method - subclass MUST implement this
13  fuelType() { throw new Error('fuelType() must be implemented'); }
14}
15
16class ElectricCar extends Vehicle {
17  fuelType() { return 'Electric'; }
18}
19
20class PetrolBike extends Vehicle {
21  fuelType() { return 'Petrol'; }
22}
23
24const car  = new ElectricCar('Tesla');
25car.stop();                      // Inherited - Tesla stopped
26console.log(car.fuelType());     // Own implementation - Electric
27

4. What does the 'this' keyword refer to in OOP?

Inside a class, this refers to the current instance - the specific object that is currently executing the method. It's how an object refers to itself. When you write this.name inside a method, you're saying 'the name property of THIS particular object, not some other object of the same class'.

In JavaScript specifically, this is famously tricky because its value depends on how a function is called, not where it's defined. Arrow functions don't have their own this - they inherit it from the surrounding scope. This is why you'll often see arrow functions used for callbacks inside class methods.

javascript
1class Timer {
2  constructor(name) {
3    this.name  = name;
4    this.count = 0;
5  }
6
7  // โœ— Regular function - 'this' is lost when used as a callback
8  startBroken() {
9    setInterval(function() {
10      this.count++; // 'this' is undefined here - ReferenceError!
11      console.log(this.count);
12    }, 1000);
13  }
14
15  // โœ… Arrow function - inherits 'this' from the class method
16  startFixed() {
17    setInterval(() => {
18      this.count++; // 'this' correctly refers to the Timer instance
19      console.log(`${this.name}: ${this.count}`);
20    }, 1000);
21  }
22}
23
24const timer = new Timer('Countdown');
25timer.startFixed();
26// Countdown: 1
27// Countdown: 2 ...
28

5. What is the difference between static and instance members?

Instance members belong to a specific object. Every object gets its own copy. Static members belong to the class itself - not to any individual object. All instances share the same static member. You access instance members through an object; you access static members through the class name.

javascript
1class User {
2  static userCount = 0;  // belongs to the class - shared across ALL users
3
4  constructor(name) {
5    this.name = name;     // belongs to this specific user object
6    User.userCount++;     // increment the shared counter
7  }
8
9  greet() {
10    return `Hi, I'm ${this.name}`; // uses instance data
11  }
12
13  static getTotalUsers() {
14    return User.userCount; // static method accesses static data
15    // Can't use 'this.name' here - no specific instance involved
16  }
17}
18
19const alice = new User('Alice');
20const bob   = new User('Bob');
21const carol = new User('Carol');
22
23console.log(alice.greet());         // Hi, I'm Alice
24console.log(User.getTotalUsers());  // 3
25
26// Static accessed via class name, not object
27// console.log(alice.userCount); // undefined - wrong way
28console.log(User.userCount);      // 3 - correct way
29

Static members are great for utility functions (Math.random()), factory methods, or tracking data that's shared across all instances like a count, a shared config, or a connection pool.

6. What is multiple inheritance and what problems does it cause?

Multiple inheritance is when a class inherits from more than one parent class simultaneously. On the surface, it sounds great - your class gets the features of both parents. The problem is when both parents have a method with the same name. Which version does the child class inherit? This ambiguity is called the Diamond Problem.

Imagine Class D inherits from both Class B and Class C, and both B and C inherit from Class A. If A defines a method called show(), and both B and C override it differently - which version of show() does D get? The answer is ambiguous, and different languages handle it differently.

LanguageMultiple Inheritance?Solution
C++Yes - fully supportedVirtual inheritance to resolve diamond problem
JavaNo - only single class inheritanceImplements multiple interfaces instead
PythonYes - supportedMRO (Method Resolution Order) - C3 linearisation algorithm
JavaScriptNo - single prototype chainMixins pattern for combining behaviours

7. What is the 'super' keyword used for?

super is how a child class talks to its parent. It has two main uses: calling the parent's constructor (you must do this before you can use this in a child constructor), and calling a parent method that the child has overridden (when you want to extend the parent's behaviour rather than completely replace it).

javascript
1class Animal {
2  constructor(name, sound) {
3    this.name  = name;
4    this.sound = sound;
5  }
6
7  describe() {
8    return `${this.name} makes a ${this.sound} sound`;
9  }
10}
11
12class Dog extends Animal {
13  constructor(name, breed) {
14    super(name, 'Woof'); // MUST call super() before using 'this'
15    this.breed = breed;
16  }
17
18  describe() {
19    // Call parent's describe() and add to it - don't rewrite it
20    const base = super.describe();
21    return `${base}. It is a ${this.breed}.`;
22  }
23}
24
25const dog = new Dog('Bruno', 'Labrador');
26console.log(dog.describe());
27// Bruno makes a Woof sound. It is a Labrador.
28

8. What is the difference between composition and inheritance?

This question separates good OOP developers from great ones. The classic phrase is: 'favour composition over inheritance'. Inheritance models an IS-A relationship. Composition models a HAS-A relationship.

Here's where inheritance causes problems: a Bird IS AN Animal - that's fine. But what happens when you have a FlyingFish? It's both a fish and something that can fly. Or a Penguin - it's a bird that cannot fly. Suddenly your inheritance tree breaks. Composition solves this by making behaviours (flying, swimming) separate objects that you attach to whatever class needs them.

javascript
1// โœ— INHERITANCE approach - gets messy with edge cases
2class Bird { fly() { console.log('Flying!'); } }
3class Penguin extends Bird {
4  fly() { throw new Error('Penguins cannot fly!'); } // awkward
5}
6
7// โœ… COMPOSITION approach - plug in only what each class needs
8const canFly = {
9  fly() { console.log(`${this.name} is flying`); },
10};
11
12const canSwim = {
13  swim() { console.log(`${this.name} is swimming`); },
14};
15
16const canRun = {
17  run() { console.log(`${this.name} is running`); },
18};
19
20// Eagle: can fly and run - no swimming
21class Eagle {
22  constructor(name) { this.name = name; }
23}
24Object.assign(Eagle.prototype, canFly, canRun);
25
26// Penguin: can swim and run - no flying
27class Penguin {
28  constructor(name) { this.name = name; }
29}
30Object.assign(Penguin.prototype, canSwim, canRun);
31
32// FlyingFish: can fly and swim
33class FlyingFish {
34  constructor(name) { this.name = name; }
35}
36Object.assign(FlyingFish.prototype, canFly, canSwim);
37
38const eagle = new Eagle('Eddie');
39eagle.fly();   // Eddie is flying
40eagle.run();   // Eddie is running
41// eagle.swim() - not available - correct!
42

9. What is coupling and cohesion in OOP?

These two concepts describe the quality of your code structure - how well your classes are designed. The goal you're always aiming for: low coupling and high cohesion.

Coupling measures how much one class depends on another. Tightly coupled classes are like those friends where if one gets sick, you hear about it from all the others. When Class A and Class B are tightly coupled, changing A forces you to change B too - and probably C and D as well. Loosely coupled classes are independent - you can swap one out without touching the others.

Cohesion measures how focused a class is. A highly cohesive class does one thing and does it well. A low cohesion class does a bit of everything and ends up doing nothing cleanly. If you find yourself naming a class 'Utilities' or 'Manager' and it has 40 methods covering 10 different concerns - that's low cohesion.

ConceptGoalProblem when wrong
CouplingLOW coupling - classes should be as independent as possibleHigh coupling = changing one class breaks many others
CohesionHIGH cohesion - a class should focus on one responsibilityLow cohesion = classes that do too much are hard to test and maintain

10. What are the different types of inheritance?

Inheritance comes in several structural patterns. Understanding them helps you design class hierarchies that make sense and avoid the traps that come with overly complex inheritance chains.

TypeDescriptionExample
SingleOne child inherits from one parentDog extends Animal
MultilevelA chain: grandchild inherits from child which inherits from parentPuppy extends Dog extends Animal
HierarchicalMultiple children inherit from one parentDog, Cat, Bird all extend Animal
MultipleOne child inherits from multiple parents (C++, Python)FlyingCar extends Car and Airplane
HybridA combination of the above types - can lead to the Diamond ProblemMix of multilevel + multiple

III. Advanced Level

1. What are the SOLID principles?

SOLID is a set of five design principles introduced by Robert C. Martin (Uncle Bob) for writing object-oriented code that is easy to maintain, extend, and test. If you find your codebase getting painful to work with - classes that do too much, code that breaks when you touch anything, or impossible-to-test functions - violating SOLID is usually why.

LetterPrincipleOne-liner
SSingle Responsibility PrincipleA class should have only one reason to change
OOpen/Closed PrincipleOpen for extension, closed for modification
LLiskov Substitution PrincipleSubtypes must be substitutable for their base types
IInterface Segregation PrincipleClients shouldn't be forced to implement methods they don't use
DDependency Inversion PrincipleDepend on abstractions, not concrete implementations

2. Explain the Single Responsibility Principle with an example.

SRP says a class should have only one reason to change. Not one method. Not one feature. One reason to change - meaning it should serve one primary purpose. If you can think of two different business stakeholders who might ask you to modify the same class for different reasons, it's violating SRP.

A classic example is a User class that handles user data AND sends emails AND generates PDF reports. If the marketing team wants to change email templates, you modify User. If the billing team wants a new PDF layout, you modify User. If the database team changes the schema, you modify User. Three different teams, three different reasons - clear SRP violation.

javascript
1// โœ— VIOLATES SRP - this class does three completely different things
2class User {
3  constructor(name, email) {
4    this.name  = name;
5    this.email = email;
6  }
7
8  saveToDatabase() { /* ... */ }  // reason 1: persistence
9  sendWelcomeEmail() { /* ... */ } // reason 2: email communication
10  generateProfilePDF() { /* ... */ } // reason 3: document generation
11}
12
13// โœ… FOLLOWS SRP - each class has ONE job
14class User {
15  constructor(name, email) {
16    this.name  = name;
17    this.email = email;
18  }
19}
20
21class UserRepository {
22  save(user) { /* handle DB persistence */ }
23  findById(id) { /* ... */ }
24}
25
26class EmailService {
27  sendWelcome(user) { /* handle email logic */ }
28}
29
30class PDFGenerator {
31  generateUserProfile(user) { /* handle PDF creation */ }
32}
33
34// Now each class has exactly one reason to change.
35// Changing email templates? Touch only EmailService.
36// Changing DB schema? Touch only UserRepository.
37

3. What is the Open/Closed Principle?

Open/Closed says your code should be open for extension but closed for modification. In plain English: you should be able to add new functionality without going into existing, working code and changing it. Because every time you touch existing code, you risk introducing bugs.

The classic OCP violation is a giant if-else or switch statement that grows every time you add a new type. Need to support a new payment method? You go back into the processPayment function and add another else if. That is the modification that OCP is trying to eliminate. Instead, design a system where adding a new type means creating a new class - not editing old ones.

javascript
1// โœ— VIOLATES OCP - every new discount type requires editing this function
2function applyDiscount(user, price) {
3  if (user.type === 'student')  return price * 0.80;
4  if (user.type === 'senior')   return price * 0.75;
5  if (user.type === 'employee') return price * 0.60;
6  // adding 'military' means editing this function again...
7  return price;
8}
9
10// โœ… FOLLOWS OCP - add new discount by creating a new class, zero existing code changed
11class StudentDiscount  { apply(price) { return price * 0.80; } }
12class SeniorDiscount   { apply(price) { return price * 0.75; } }
13class EmployeeDiscount { apply(price) { return price * 0.60; } }
14class MilitaryDiscount { apply(price) { return price * 0.70; } } // NEW - no old code touched
15
16function applyDiscount(price, discountStrategy) {
17  return discountStrategy.apply(price);
18}
19
20console.log(applyDiscount(1000, new StudentDiscount()));  // 800
21console.log(applyDiscount(1000, new MilitaryDiscount())); // 700
22

4. What is the Liskov Substitution Principle?

LSP says that if you have code that works with a Parent class, you should be able to drop in any subclass of Parent and the code should still work correctly - without needing to check what type it is. Sounds obvious. But it's violated more often than you'd think.

The most famous example is the Square-Rectangle problem. Mathematically, a Square IS a Rectangle. So extending Rectangle with Square seems natural. But if Rectangle has setWidth and setHeight methods, and you apply them to a Square (which must keep sides equal), suddenly setting the width changes the height too - breaking the expectation that width and height are independent. The Penguin-Bird example from earlier is another classic violation.

javascript
1// โœ— LSP VIOLATION - Square is NOT a proper substitute for Rectangle
2class Rectangle {
3  constructor(w, h) { this.width = w; this.height = h; }
4  setWidth(w)  { this.width  = w; }
5  setHeight(h) { this.height = h; }
6  area() { return this.width * this.height; }
7}
8
9class Square extends Rectangle {
10  setWidth(w)  { this.width = this.height = w; } // must keep both equal
11  setHeight(h) { this.width = this.height = h; } // same
12}
13
14// This function SHOULD work with any rectangle
15function testRectangle(rect) {
16  rect.setWidth(5);
17  rect.setHeight(3);
18  console.log(rect.area()); // Expected: 15
19}
20
21testRectangle(new Rectangle(0, 0)); // 15 โœ…
22testRectangle(new Square(0));       // 9 โœ— - LSP violated!
23// Square is not a proper substitute for Rectangle in this context
24
25// โœ… FIX - model them as separate shapes sharing a common interface
26class Shape { area() { throw new Error('Not implemented'); } }
27class Rect   extends Shape { constructor(w,h){super();this.w=w;this.h=h;} area(){return this.w*this.h;} }
28class Sq     extends Shape { constructor(s){super();this.s=s;}           area(){return this.s**2;} }
29

5. What is the Interface Segregation Principle?

ISP says don't force a class to implement methods it doesn't need. If you have a fat interface with 10 methods and a class only needs 3 of them, it shouldn't have to implement the other 7 (even as empty stubs). Instead, split that fat interface into smaller, more focused ones.

javascript
1// โœ— VIOLATES ISP - one fat interface forces Robot to implement eat() and sleep()
2class Worker {
3  work()  { throw new Error('Not implemented'); }
4  eat()   { throw new Error('Not implemented'); }
5  sleep() { throw new Error('Not implemented'); }
6}
7
8class Robot extends Worker {
9  work()  { console.log('Robot working'); }
10  eat()   { throw new Error('Robots do not eat!'); } // forced to implement nonsense
11  sleep() { throw new Error('Robots do not sleep!'); }
12}
13
14// โœ… FOLLOWS ISP - split into focused interfaces
15class Workable  { work()  { throw new Error('Not implemented'); } }
16class Feedable  { eat()   { throw new Error('Not implemented'); } }
17class Restable  { sleep() { throw new Error('Not implemented'); } }
18
19// Human needs all three - compose them
20class Human {
21  work()  { console.log('Human working'); }
22  eat()   { console.log('Human eating');  }
23  sleep() { console.log('Human sleeping'); }
24}
25
26// Robot only needs Workable - clean!
27class Robot {
28  work() { console.log('Robot working'); }
29  // No eat() or sleep() - not relevant and not forced
30}
31

6. What is the Dependency Inversion Principle?

DIP has two parts: high-level modules should not depend on low-level modules (both should depend on abstractions), and abstractions should not depend on details (details should depend on abstractions). In plain English: your business logic should not be directly wired to specific implementations. It should talk to interfaces.

This is the principle behind dependency injection. Instead of creating your dependencies inside a class (which hard-wires them), you receive them from outside. This makes testing dramatically easier - you can inject a mock database instead of a real one.

javascript
1// โœ— VIOLATES DIP - OrderService is hard-wired to MySQLDatabase
2class MySQLDatabase {
3  save(order) { console.log('Saving to MySQL...'); }
4}
5
6class OrderService {
7  constructor() {
8    this.db = new MySQLDatabase(); // tight coupling - can't swap this out
9  }
10  placeOrder(order) { this.db.save(order); }
11}
12
13// โœ… FOLLOWS DIP - OrderService depends on an abstraction, not a specific DB
14class MongoDatabase {
15  save(order) { console.log('Saving to MongoDB...'); }
16}
17
18class PostgresDatabase {
19  save(order) { console.log('Saving to Postgres...'); }
20}
21
22class OrderService {
23  constructor(database) {     // dependency is INJECTED from outside
24    this.db = database;
25  }
26  placeOrder(order) { this.db.save(order); }
27}
28
29// Production
30const service = new OrderService(new MongoDatabase());
31service.placeOrder({ id: 1 }); // Saving to MongoDB...
32
33// Testing - inject a mock, no real DB needed
34const mockDb = { save: (order) => console.log('Mock save', order) };
35const testService = new OrderService(mockDb);
36testService.placeOrder({ id: 1 }); // Mock save { id: 1 }
37

7. What are design patterns in OOP? Name the categories.

Design patterns are reusable solutions to commonly occurring problems in software design. They're not code you copy-paste - they're templates for how to structure your code to solve a particular type of problem. The 'Gang of Four' book (published in 1994) catalogued 23 of them, grouped into three categories.

CategoryWhat it handlesKey patterns
CreationalHow objects are created - controlling instantiationSingleton, Factory, Abstract Factory, Builder, Prototype
StructuralHow classes and objects are composed into larger structuresAdapter, Bridge, Composite, Decorator, Facade, Proxy
BehaviouralHow objects communicate and assign responsibilitiesObserver, Strategy, Command, Iterator, Template Method, State

8. What is the Singleton design pattern and when do you use it?

Singleton ensures that a class has only one instance throughout the entire lifecycle of your application, and provides a global access point to that instance. It solves the problem of needing exactly one of something - one database connection pool, one logger, one configuration manager, one cache.

The pattern works by making the constructor private (so nothing can call new directly) and providing a static method that either returns the existing instance or creates one if it doesn't exist yet.

javascript
1class DatabaseConnection {
2  static #instance = null; // the one and only instance
3
4  #connectionString;
5  #isConnected = false;
6
7  constructor(connectionString) {
8    this.#connectionString = connectionString;
9  }
10
11  static getInstance(connectionString) {
12    if (!DatabaseConnection.#instance) {
13      // First time - create the instance
14      DatabaseConnection.#instance = new DatabaseConnection(connectionString);
15      console.log('DB connection created');
16    }
17    // Always return the same instance
18    return DatabaseConnection.#instance;
19  }
20
21  connect() {
22    if (!this.#isConnected) {
23      this.#isConnected = true;
24      console.log(`Connected to ${this.#connectionString}`);
25    }
26  }
27
28  query(sql) {
29    return `Result of: ${sql}`;
30  }
31}
32
33// Both of these get the EXACT same object
34const db1 = DatabaseConnection.getInstance('mongodb://localhost:27017/app');
35const db2 = DatabaseConnection.getInstance('mongodb://localhost:27017/app');
36
37console.log(db1 === db2); // true - same instance
38
39// Use with care: Singleton is a global state
40// It makes testing harder because you can't isolate the instance
41// Great for: loggers, config, connection pools, caches
42// Avoid for: anything that needs to vary between tests
43

9. What is the Factory design pattern?

The Factory pattern solves the problem of creating objects without specifying their exact class. Instead of calling new SomeClass() all over your codebase, you call a factory method and let it figure out which class to instantiate based on the input. This is incredibly useful when the exact type of object needed depends on runtime conditions.

Real-world example: a notification system. Depending on the user's preference, you might need to send an email, an SMS, or a push notification. You don't want your business logic peppered with if-email, if-sms checks. A factory handles that decision for you.

javascript
1// Each notification type knows how to send itself
2class EmailNotification {
3  send(message) { console.log(`๐Ÿ“ง Email: ${message}`); }
4}
5
6class SMSNotification {
7  send(message) { console.log(`๐Ÿ“ฑ SMS: ${message}`); }
8}
9
10class PushNotification {
11  send(message) { console.log(`๐Ÿ”” Push: ${message}`); }
12}
13
14// The Factory - one place that handles the 'which class?' decision
15class NotificationFactory {
16  static create(type) {
17    switch (type.toLowerCase()) {
18      case 'email': return new EmailNotification();
19      case 'sms':   return new SMSNotification();
20      case 'push':  return new PushNotification();
21      default: throw new Error(`Unknown notification type: ${type}`);
22    }
23  }
24}
25
26// Business logic never uses 'new' directly
27// It just asks the factory for what it needs
28function notifyUser(user, message) {
29  const notifier = NotificationFactory.create(user.preferredChannel);
30  notifier.send(message);
31}
32
33notifyUser({ preferredChannel: 'email' }, 'Your order is confirmed!');
34// ๐Ÿ“ง Email: Your order is confirmed!
35
36notifyUser({ preferredChannel: 'push' }, 'Flash sale starts now!');
37// ๐Ÿ”” Push: Flash sale starts now!
38
39// Adding WhatsApp? Create WhatsAppNotification class,
40// add one case to the factory - zero other code touched
41

10. What are DRY, KISS, and YAGNI principles?

These three principles aren't OOP-exclusive but they're deeply connected to writing good OOP code. They're the kind of principles that sound obvious when you read them but are constantly violated in real codebases - including by experienced developers.

PrincipleStands forWhat it meansClassic violation
DRYDon't Repeat YourselfEvery piece of knowledge should have a single, authoritative representation in your system. Duplicated logic means bugs get fixed in one place but not the other.Copy-pasting a validation block in 5 different places instead of putting it in one shared function
KISSKeep It Simple, StupidSimpler code is easier to read, test, debug, and maintain. Complexity should only be added when necessary - not because it shows off skill.Building a fully abstracted plugin architecture for a script that runs once a month
YAGNIYou Aren't Gonna Need ItDon't build features or abstractions until you actually need them. Code written speculatively for 'future requirements' is technical debt you're paying interest on now.Adding 12 configuration options to a class 'just in case' when the app has 3 users

A quick way to remember all three: DRY tells you not to repeat code, KISS tells you not to complicate code, and YAGNI tells you not to write code you don't need yet. All three push in the same direction - towards lean, focused, maintainable software. The best developers obsess over these three just as much as they obsess over design patterns.

Found this helpful?

Share it with your network

Related Articles

Frontend

JavaScript

Prepare for your next tech interview with the most asked JavaScript interview questions and answers. It includes basic to advanced concepts, coding problems, and real-world scenarios for freshers and experienced developers.

Frontend

Java

Prepare for your next tech interview with the most asked Java interview questions and answers. It includes basic to advanced concepts, coding problems, and real-world scenarios for freshers and experienced developers.