MindForge
MindForge
DashboardHelp & Support
Dashboard
MindForge

Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
OOP Foundations for Design
Course contents

Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
Browsing without an account — progress is saved in this browser only.Sign in to save it

OOP Foundations for Design

Reading50 min

Almost everyone can recite "encapsulation, abstraction, inheritance, polymorphism" from memory. Far fewer can say why encapsulation actually matters when someone is reviewing their code, or when inheritance is the wrong tool, or what "tight coupling" costs in a real bug report. This lesson closes that gap. You'll leave it able to defend each pillar as a decision, not just name it.

The four pillars, as decisions you can defend

Encapsulation — hide the data, expose safe actions. This isn't about typing private in front of a field. The real point is: an object should never let outside code put it into a broken state.

Picture a bank account. If any code anywhere can just set balance = -500, the rule "balance never goes negative" has to be remembered and re-checked by every single piece of code that touches an account. That's fragile. Move the rule inside the account instead, and it only has to be right once.

Python
# Weak: the rule "balance never goes negative" lives in every caller's head.
class BadAccount:
    def __init__(self):
        self.balance = 0          # anyone can write anything here

# Strong: the rule lives in one place, and the class enforces it.
class Account:
    def __init__(self, opening_paise: int = 0):
        if opening_paise < 0:
            raise ValueError("opening balance cannot be negative")
        self._balance = opening_paise      # private by convention

    @property
    def balance(self) -> int:              # read-only view
        return self._balance

    def deposit(self, paise: int) -> None:
        if paise <= 0:
            raise ValueError("deposit must be positive")
        self._balance += paise

    def withdraw(self, paise: int) -> None:
        if paise > self._balance:
            raise ValueError("insufficient funds")
        self._balance -= paise


acct = Account(10_000)
acct.deposit(5_000)
acct.withdraw(2_000)
assert acct.balance == 13_000

for bad in (lambda: acct.withdraw(999_999), lambda: acct.deposit(-1), lambda: Account(-5)):
    try:
        bad()
        raise AssertionError("invalid operation was allowed")
    except ValueError:
        pass
print("invariants held; balance =", acct.balance)

Here's the payoff: if a bug report says "balance went negative," you now have exactly three methods to check, not the whole codebase. And here's a trap worth knowing: writing a public getter and a public setter for every field is not encapsulation. A setter that accepts anything is just a public field wearing a disguise. Real encapsulation means exposing actions (withdraw) that keep the object's rules intact, not raw access to its fields.

Abstraction — show what something does, hide how. PaymentProcessor.charge(amount) tells you nothing about whether it's Stripe or Razorpay underneath, so your code never has to care. Abstraction is what lets you swap one for the other later without touching anything else.

People mix up abstraction and encapsulation constantly, so here's the one line that separates them: encapsulation hides data. Abstraction hides implementation.

Abstraction Encapsulation
Hides Complexity — the how Data — the internal state
Goal Give a simple front door to something complicated Stop invalid changes to internal state
Typical tool An interface, an abstract class, a well-named method Private fields, properties
Example car.start() hides the ignition sequence and fuel injection self.__balance can't be poked from outside the class
Python
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance  # encapsulation: name-mangled, not reachable from outside

    def deposit(self, amount):    # abstraction: the caller never sees HOW this is checked
        if amount <= 0:           # or stored — just that deposit() does the right thing
            raise ValueError("Deposit must be positive")
        self.__balance += amount

    @property
    def balance(self):
        return self.__balance     # controlled read access — this is the encapsulation part

deposit() is abstraction: it gives the caller one simple verb and hides the validation underneath. __balance plus the read-only balance property is encapsulation: it stops account.__balance = -1000 from skipping the rules entirely (Python renames __balance internally so outside code can't reach it directly). The two work as a pair: abstraction shapes the interface, encapsulation decides who gets to touch what behind it.

Inheritance — share a contract, not a shortcut to reuse code. The one legitimate reason to inherit is that the child is genuinely, always substitutable for the parent — a SavingsAccount really is an Account everywhere an Account is expected. Inheriting purely to avoid retyping a method is the single most common design mistake in this whole topic, and the next section shows exactly why.

Polymorphism — one call, many behaviours. This is the thing that gets rid of long chains of if type == ....

Python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

class Circle(Shape):
    def __init__(self, r: float): self.r = r
    def area(self) -> float: return 3.14159 * self.r ** 2

class Rectangle(Shape):
    def __init__(self, w: float, h: float): self.w, self.h = w, h
    def area(self) -> float: return self.w * self.h

# One line calls .area() on anything. Adding Triangle means adding a class — nothing here changes.
shapes: list[Shape] = [Circle(1), Rectangle(2, 3)]
total = sum(s.area() for s in shapes)
assert abs(total - 9.14159) < 1e-6
print(f"total area = {total:.5f}")

Here's a quick test for whether polymorphism is doing real work: can you add a new type without touching any existing code? If adding Triangle means hunting down an if shape_type == ... chain somewhere and editing it, you've written types without actually using polymorphism.

Python doesn't even require a shared parent for this to work. This is called duck typing: "if it walks like a duck and quacks like a duck, treat it like a duck."

Python
class Circle:
    def __init__(self, r): self.r = r
    def area(self): return 3.14159 * self.r ** 2

class Square:                      # note: does NOT inherit from anything
    def __init__(self, side): self.side = side
    def area(self): return self.side ** 2

for shape in [Circle(2), Square(3)]:
    print(shape.area())            # works on both — Python never checks a shared ancestor

When that loop runs, Python doesn't ask "is this a Shape?" anywhere. It just calls whatever .area() method the object happens to have. This is looser than Java, where the compiler checks a declared interface before your code even runs.

Operator overloading is polymorphism applied to symbols like + and ==. Python routes those symbols to methods with special names:

Python
class Vector:
    def __init__(self, x, y): self.x, self.y = x, y
    def __add__(self, other): return Vector(self.x + other.x, self.y + other.y)
    def __eq__(self, other): return self.x == other.x and self.y == other.y

v = Vector(1, 2) + Vector(3, 4)  # this line is really Vector(1, 2).__add__(Vector(3, 4))

Python has no special-case rule for + on a Vector. The + symbol is defined to call .__add__() on whatever sits on its left, and Vector happens to provide one. That call returns a brand new Vector(4, 6).

Remember: encapsulation hides data, abstraction hides how something works. A public getter-and-setter pair is not encapsulation — it's a field wearing a disguise.

Sign in to try this knowledge check.

Composition over inheritance

This is the single most useful habit in this whole topic, and it's the one interviewers lean on hardest.

Here's the problem with inheritance. It's the tightest coupling a language offers — a subclass depends on the parent's actual implementation, not just its promises. Change the parent, and every child might quietly break. This is called the "fragile base class" problem. Worse, inheritance can only express one dimension of change at a time. The moment two things vary independently, your class tree explodes.

Vehicle
├── ElectricCar
├── PetrolCar
├── ElectricTruck        this explodes:
├── PetrolTruck          (fuel type) × (body type) needs one class PER combination
├── ElectricMotorcycle
└── ...

The fix: make the varying part something the object has, not something it is.

from abc import ABC, abstractmethod

class Engine(ABC):
    @abstractmethod
    def start(self) -> str: ...

class PetrolEngine(Engine):
    def start(self) -> str: return "vroom"

class ElectricMotor(Engine):
    def start(self) -> str: return "hum"

class Vehicle:
    """One class. The thing that varies is a field, not a branch of a class tree."""
    def __init__(self, name: str, engine: Engine, wheels: int):
        self.name, self.engine, self.wheels = name, engine, wheels

    def start(self) -> str:
        return f"{self.name} ({self.wheels} wheels): {self.engine.start()}"


car = Vehicle("car", PetrolEngine(), 4)
bike = Vehicle("bike", ElectricMotor(), 2)
assert car.start().endswith("vroom") and bike.start().endswith("hum")

# You can even swap the engine while the program is running — inheritance can't do this.
car.engine = ElectricMotor()
assert car.start().endswith("hum")
print(car.start(), "|", bike.start())
Inheritance Composition
Relationship IS-A HAS-A
Decided When the code is written While the program runs — swappable
Coupling Tight — to the parent's actual code Loose — only to an interface
Two things varying at once Class count explodes One field per varying thing
Testing Must build the whole family tree Just hand it a fake collaborator

When inheritance is genuinely the right call: a real IS-A relationship where the child can stand in for the parent everywhere (a SavingsAccount really is an Account), a stable parent you control, and only one thing varying. A shallow tree — one or two levels — is usually fine and often the clearest option.

Here's a sentence worth having ready: "I'd use composition here, because fuel type and body type vary independently. Inheritance would need one class per combination."

Remember: the moment two things vary independently, stop reaching for subclasses and start reaching for a field.

Sign in to try this knowledge check.

Coupling and cohesion

These are the two words a reviewer actually means when they say a design is "clean."

Coupling — how much one piece of code has to know about another. Lower is better.

Level What it looks like Verdict
Data coupling Passing a plain number Best
Stamp coupling Passing a whole object when only one field gets used Fine, a little wasteful
Control coupling Passing a flag that changes what the method does (save(dryRun=True)) A smell — usually should be two methods
Common coupling Shared global state anyone can change Bad — dependencies become invisible
Content coupling Reaching straight into another object's insides (order.items[0].price = 0) Worst

Two quick tests to run on any design: if I change class A, how many other classes have to change too? And: how many objects do I need to build just to test A on its own? Both answers should be small.

Cohesion — how well the contents of one class actually belong together. Higher is better. A cohesive class has a name that tells you exactly what it does. A class with low cohesion tends to be named Utils, Manager, or Helper, and different methods inside it use completely different subsets of its fields.

There's a rule that makes low coupling concrete, called the Law of Demeter: only talk to your immediate friends.

Python
# A long chain — this method now depends on the shape of Order, Customer, AND Address.
def bad(order):
    return order.get_customer().get_address().get_city().upper()

# Ask the object you already have, and let IT answer the question.
def good(order):
    return order.shipping_city().upper()

Every extra . past the first one is one more thing that can break your code later.

A five-item checklist you can run against any design, with the standard fix for each:

Smell What it looks like Fix
God class OrderManager with 40 methods Split it up by why each part would change
Feature envy A method that mostly reads some other class's fields Move the method to the class that owns the data
Primitive obsession A plain string standing in for money, an id, a phone number Wrap it in a value object: Money, PhoneNumber, TicketId
Shotgun surgery One small change means editing seven different files The idea is scattered — pull it into one class
Long parameter list create(a, b, c, d, e, f, g) Bundle the parameters into an object, or use a builder

Remember: every extra dot past the first one in a chain is a dependency you just took on. Ask the thing you already hold, don't reach through it.

Sign in to try this knowledge check.

Interfaces, abstract classes, and dependency injection

Interface vs. abstract class shows up in almost every LLD interview, and the answer really comes down to one question: what are you sharing?

Abstract class means "IS-A." Use it when the things involved share both a type relationship and some actual code. A Dog is an Animal.

Interface means "CAN-DO." Use it when unrelated things need to promise the same capability, with no code in common. A Dog can Swim; so can a Boat, and a Boat is definitely not an Animal.

Interface Abstract class
Shares Just a promise — "here's what I can do" A promise, plus some real code
Holds state No (constants only) Yes — fields, a constructor
How many A class can implement many One parent only, in most languages
Answers "What can you do, no matter what you are?" "What kind of thing are you, and what do you already do?"
Cost of a change Adding a method breaks everything that implements it Adding a concrete method breaks nobody

In Python specifically, there's no separate abstract class keyword. abc.ABC plus @abstractmethod gives you the same guarantee:

Python
from abc import ABC, abstractmethod

class Shape(ABC):
    def __init__(self, color: str):
        self.color = color          # shared state — every Shape has a colour

    def describe(self) -> str:      # a real method, shared by every subclass
        return f"A {self.color} shape with area {self.area():.2f}"

    @abstractmethod
    def area(self) -> float: ...    # every subclass MUST provide this

class Circle(Shape):
    def __init__(self, color: str, radius: float):
        super().__init__(color)
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2

Shape("red")     # TypeError: Can't instantiate abstract class Shape
Circle("red", 2).describe()   # "A red shape with area 12.57"

Trying to build a plain Shape raises a TypeError the instant you try. Python checks this while the program is running, not before, since there's no separate compile step. A subclass that forgets to write area() is also unbuildable, and the error only shows up the first time someone actually tries to construct it.

Python's real answer to "CAN-DO" is typing.Protocol, not a keyword. Java's implements requires you to declare the relationship up front — that's called nominal typing. Python's Protocol just checks the shape of an object — that's structural typing:

Python
from typing import Protocol

class Swimmer(Protocol):
    def swim(self) -> str: ...

class Dog:
    def swim(self) -> str:
        return "paddles"

class Boat:
    def swim(self) -> str:
        return "cuts through water"

def race(swimmer: Swimmer) -> str:
    return swimmer.swim()

race(Dog())   # works — Dog never said "I implement Swimmer", it just has the method
race(Boat())  # also works — Boat has nothing to do with Dog at all

Neither Dog nor Boat inherits from Swimmer. They satisfy it just by happening to have a matching method. When race(Dog()) runs, Python doesn't check any declared relationship at all — it just calls .swim() and trusts that whatever was handed to it has one. That's the concrete Python answer to "CAN-DO": no shared parent, no shared code, just a shared ability, checked either by a type checker or simply by trying it at runtime.

Abstract classes earn their keep through the Template Method pattern: the abstract class nails down the order of a series of steps, and each subclass only fills in the steps that actually change.

Python
class DataPipeline(ABC):
    def run(self) -> None:              # the fixed order — never overridden
        data = self.extract()
        cleaned = self.transform(data)
        self.load(cleaned)

    @abstractmethod
    def extract(self): ...
    @abstractmethod
    def transform(self, data): ...
    @abstractmethod
    def load(self, data): ...

class CsvPipeline(DataPipeline):
    def extract(self): return open("in.csv").read()
    def transform(self, data): return data.upper()
    def load(self, data): open("out.csv", "w").write(data)

run() never changes. The shape of the process is fixed in the parent class, and only the individual steps vary per subclass. Calling CsvPipeline().run() runs DataPipeline.run, which calls extract, transform, and load in that exact order — but each one resolves to CsvPipeline's own version. This is the answer to "why not just give me three methods and let me call them in the right order myself?" The abstract class owns and guarantees the order, so nobody can call the steps out of sequence or skip one by mistake.

Two interview questions worth having answers ready for:

"Can an abstract class have a constructor if you can't build one directly?" Yes — it runs whenever a subclass is built, through super().__init__(). You can't construct the parent alone, but its constructor is still part of building every child.

"Why not just use Strategy instead of Template Method?" Strategy swaps out a whole algorithm through composition — you hand in a different object. Template Method fixes the order of steps at write-time and only lets subclasses change individual steps. Pick Template Method when the sequence must never change. Pick Strategy when you need to swap the entire thing while the program runs.

Dependency injection is what turns "depend on an interface" from advice into working code: a class receives the things it needs from outside, instead of building them itself.

Python
class SmtpMailer:
    def send(self, to: str, body: str) -> None:
        print(f"[smtp] to {to}: {body}")

class FakeMailer:
    def __init__(self): self.sent: list[tuple[str, str]] = []
    def send(self, to: str, body: str) -> None: self.sent.append((to, body))

class SignupService:
    def __init__(self, mailer):            # handed in — not `self.mailer = SmtpMailer()`
        self.mailer = mailer

    def register(self, email: str) -> None:
        self.mailer.send(email, "Welcome!")


fake = FakeMailer()
SignupService(fake).register("a@mindforge.test")
assert fake.sent == [("a@mindforge.test", "Welcome!")]   # tested with no real mail server
SignupService(SmtpMailer()).register("b@mindforge.test")
print("sent:", fake.sent)

Building your own dependency inside the class (self.mailer = SmtpMailer()) locks the class to one implementation, makes it impossible to test without real infrastructure, and hides what the class actually needs from anyone reading its constructor. Injecting it instead puts every dependency right there in the signature — which is also why a constructor with eight parameters is a warning sign: that class is doing eight different jobs.

Remember: interface means "can do," abstract class means "is a." When in doubt, ask what's actually being shared — a promise, or a promise plus real code.

Sign in to try this knowledge check.

Quick recap

Encapsulation : hide state, expose actions that keep the object valid.
                A public setter is a public field. Validate in the constructor.
Abstraction   : hide HOW something works behind a simple interface
                (encapsulation hides DATA, abstraction hides HOW)
Inheritance   : share a contract, not just code. IS-A + substitutable + one axis + shallow.
Polymorphism  : one call, many behaviours. Test: can I add a type with zero edits elsewhere?
                Python also does this via duck typing — no shared parent required.

COMPOSITION OVER INHERITANCE
  Inheritance = fixed at write-time, tight coupling, one axis → class count explodes
  Composition = swappable while running, loose coupling, one field per axis, easy to fake

Coupling (low is good): data < stamp < control < common < content   ← worst
  Law of Demeter: every dot past the first is a new dependency. Ask, don't reach through.
Cohesion (high is good): a class whose name tells you exactly what it does.
  Smells: god class · feature envy · primitive obsession · shotgun surgery · long param list

Interface = a promise only, many per class, unrelated types (CAN-DO)
Abstract class = a promise plus real code, one per class (IS-A)
  Python: ABC + @abstractmethod for "IS-A"; Protocol (structural) for "CAN-DO"
  Template Method: the abstract class fixes the ORDER, subclasses fill in the STEPS
Dependency injection: receive collaborators, never build them yourself.
  A constructor with 8 parameters means that class is doing 8 jobs.
  • Encapsulation is about who's responsible for keeping the object valid — and the answer has to be the class itself, never its callers.
  • Reach for composition first, every time, and be ready to say why: independent things that vary, swapping while the program runs, easier testing.
  • Coupling and cohesion are the words reviewers actually use. "This Manager class has low cohesion — I'd split pricing away from saving to disk" is exactly what a real design conversation sounds like.
  • Every smell in that checklist has a standard fix, and knowing the pair — smell, then fix — is what lets you improve a design live, on the spot, when the interviewer pushes back.
PreviousThe LLD Interview Framework and UMLNextSOLID Principles

Course progress

0%
0 of 177 modules complete

On this page

  • The four pillars, as decisions you can defend
  • Composition over inheritance
  • Coupling and cohesion
  • Interfaces, abstract classes, and dependency injection
  • Quick recap