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
SOLID Principles
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

SOLID Principles

Reading55 min

SOLID is the vocabulary interviewers reach for when they explain why one design beats another. That's exactly why "explain SOLID with an example" is one of the most common LLD questions there is. Just reciting the five names earns you nothing. What actually earns points is showing a bad design, naming the exact principle it breaks, and fixing it — which is exactly how each section below is built.

Hold onto this one idea: every SOLID principle exists to make change cheap. Each one answers the same question in a different way: "when the requirements change, how much code has to change with them?"

S — Single Responsibility Principle

A class should have one reason to change.

"One reason to change" is a sharper test than "does one thing." Here's the actual question to ask: who, in the real world, would ask for a change to this class? If the finance team, the design team, and the ops team could all separately trigger an edit, that class has three jobs, not one.

# BROKEN — three different people can ask for a change here, all in one class.
class BadReport:
    def compute(self, rows): ...        # finance changes the formula
    def to_pdf(self): ...               # design changes the layout
    def email(self, to): ...            # ops changes the mail server
Python
from dataclasses import dataclass

# FIXED — each class changes for exactly one reason.
@dataclass(frozen=True)
class SalesReport:
    total_paise: int
    rows: int

class ReportCalculator:              # changes when the business rule changes
    def compute(self, sales: list[int]) -> SalesReport:
        return SalesReport(total_paise=sum(sales), rows=len(sales))

class ReportPdfRenderer:             # changes when the layout changes
    def render(self, report: SalesReport) -> bytes:
        return f"PDF<total={report.total_paise},rows={report.rows}>".encode()

class ReportMailer:                  # changes when the way we deliver it changes
    def __init__(self, transport): self.transport = transport
    def send(self, to: str, document: bytes) -> None:
        self.transport.send(to, document)


class FakeTransport:
    def __init__(self): self.outbox = []
    def send(self, to, doc): self.outbox.append((to, doc))

report = ReportCalculator().compute([10_000, 25_000, 5_000])
pdf = ReportPdfRenderer().render(report)
transport = FakeTransport()
ReportMailer(transport).send("cfo@mindforge.test", pdf)

assert report.total_paise == 40_000
assert transport.outbox[0][0] == "cfo@mindforge.test"
print("report pipeline ok:", pdf.decode())

The real win here isn't tidiness — it's that the calculator can now be tested with no PDF library and no mail server at all, and a layout tweak can never accidentally break the arithmetic.

One trap to watch for: don't overdo this. Splitting one class into six classes with one tiny method each isn't SRP, it's just thin, scattered design. Group things by why they'd change, not by how few lines they have.

Remember: the test for SRP isn't "does this do one thing" — it's "could two different teams ask for two different changes here?"

Sign in to try this knowledge check.

O — Open/Closed Principle

Open for extension, closed for modification.

Adding a new behaviour should mean writing new code, never editing code that already works. The warning sign is a growing if/elif chain built around a type or a name.

Python
# BROKEN — every new payment method means editing this method, risking the ones already there.
class BadProcessor:
    def pay(self, method: str, paise: int) -> str:
        if method == "card":
            return f"charged card {paise}"
        elif method == "upi":
            return f"charged upi {paise}"
        # elif "wallet" ... elif "netbanking" ... forever
        raise ValueError(method)
Python
from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, paise: int) -> str: ...

class Card(PaymentMethod):
    def pay(self, paise: int) -> str: return f"charged card {paise}"

class Upi(PaymentMethod):
    def pay(self, paise: int) -> str: return f"charged upi {paise}"

class Wallet(PaymentMethod):                 # NEW — added, nothing else touched
    def pay(self, paise: int) -> str: return f"charged wallet {paise}"

class Checkout:
    def pay(self, method: PaymentMethod, paise: int) -> str:
        return method.pay(paise)             # this method never has to change again


checkout = Checkout()
assert checkout.pay(Card(), 49_900) == "charged card 49900"
assert checkout.pay(Wallet(), 1_000) == "charged wallet 1000"
print(checkout.pay(Upi(), 25_000))

How do you know which direction to keep open? You can't design a class that's open to literally every possible future change — that's just over-engineering with extra steps. Open it along the specific direction the requirements point to: "we'll keep adding new payment methods" means open there. "The tax formula is fixed by law and only changes once a decade" means leave that part concrete.

Where this shows up in real systems: strategy objects, plugin registries, event listeners, and — the simplest version of all — a plain dictionary lookup replacing an if/elif chain.

Remember: a growing if/elif on a type or a name is the tell. Every new branch is a risk to everything already working.

Sign in to try this knowledge check.

L — Liskov Substitution Principle

Anywhere code expects the parent type, a child type must work too, without the caller noticing the difference.

This principle decides whether inheritance was even the right call, and its violations tend to be sneaky.

Python
# BROKEN — the textbook example. A square IS a rectangle in geometry, but not safely in code.
class Rectangle:
    def __init__(self, w, h): self._w, self._h = w, h
    def set_width(self, w): self._w = w
    def set_height(self, h): self._h = h
    def area(self): return self._w * self._h

class Square(Rectangle):
    def __init__(self, side): super().__init__(side, side)
    def set_width(self, w): self._w = self._h = w      # forced to keep both sides equal
    def set_height(self, h): self._w = self._h = h

def client_expects_rectangle_behaviour(r: Rectangle) -> int:
    r.set_width(5)
    r.set_height(4)
    return r.area()          # any real Rectangle: 20

assert client_expects_rectangle_behaviour(Rectangle(1, 1)) == 20
assert client_expects_rectangle_behaviour(Square(1)) == 16      # ← this is the broken substitution
print("Square breaks the Rectangle contract: 16 != 20")

The subclass didn't add anything — it quietly took away a promise the parent made ("width and height change independently"). The real fix isn't a clever workaround, it's to stop inheriting altogether: make Shape an interface with just area(), and let Square and Rectangle be separate, unchangeable value objects that both implement it.

Four rules a child type actually has to follow:

Rule What it means What breaking it looks like
Never demand more from callers The child can't require stricter input than the parent did Parent accepts any number; child insists on a positive one
Never deliver less than promised The child must give back at least what the parent guaranteed Parent promises a sorted list; child hands back an unsorted one
Keep the parent's rules intact Whatever the parent guaranteed still has to hold Square breaking the "sides move independently" rule above
No surprise failures Callers shouldn't get an exception they never expected A subclass throws an error from a method the parent never throws from

That last row is the most common one you'll actually see in real code: a ReadOnlyList that inherits List and then throws whenever someone calls add(). If a subclass has to say "sorry, this doesn't apply to me," the class tree itself is wrong — split the interface instead, which is exactly the next principle.

Remember: if a subclass throws on a method its parent promised would work, the class tree is broken — not the caller.

Sign in to try this knowledge check.

I — Interface Segregation Principle

Nobody should be forced to depend on a method they'll never use.

Python
from abc import ABC, abstractmethod

# BROKEN — one wide interface forces a meaningless implementation.
class BadWorker(ABC):
    @abstractmethod
    def work(self): ...
    @abstractmethod
    def eat(self): ...

class Robot(BadWorker):
    def work(self): return "working"
    def eat(self): raise NotImplementedError("robots don't eat")   # ← forced to write this
Python
from abc import ABC, abstractmethod

# FIXED — small, focused interfaces; each class implements only what it actually is.
class Workable(ABC):
    @abstractmethod
    def work(self) -> str: ...

class Feedable(ABC):
    @abstractmethod
    def eat(self) -> str: ...

class Human(Workable, Feedable):
    def work(self) -> str: return "working"
    def eat(self) -> str: return "eating"

class Robot(Workable):                       # nothing meaningless to implement anymore
    def work(self) -> str: return "working"

def run_shift(workers: list[Workable]) -> list[str]:
    return [w.work() for w in workers]       # only depends on what it actually needs


assert run_shift([Human(), Robot()]) == ["working", "working"]
assert Human().eat() == "eating"
assert not hasattr(Robot, "eat")
print("segregated interfaces ok")

The practical tell: any method body that's just pass, return null, or an exception saying "not implemented." Every single one of those is proof that the interface asked for more than that class actually is.

There's a related idea worth knowing by name: role interfaces. Instead of one giant UserService with twenty methods, split it into UserReader, UserWriter, PasswordResetter. Each piece of code that uses it only depends on the two methods it actually calls, so a change to password logic can't accidentally break code that only reads users. It also makes your test doubles small — a fake with two methods instead of twenty.

Remember: a method body that just throws "not implemented" is proof the interface promised more than that class actually is.

Sign in to try this knowledge check.

D — Dependency Inversion Principle

High-level code shouldn't depend on low-level details. Both should depend on an abstraction — and the abstraction shouldn't depend on the details either.

# BROKEN — the business rule is welded directly to MySQL and to SMTP.
class BadOrderService:
    def __init__(self):
        self.db = "MySQLConnection(...)"      # builds its own low-level detail
    def place(self, order): ...               # can't test this without a real database
Python
from abc import ABC, abstractmethod

# The interface belongs to the HIGH-level module and speaks its language, not the database's.
class OrderRepository(ABC):
    @abstractmethod
    def save(self, order: dict) -> str: ...

class Notifier(ABC):
    @abstractmethod
    def notify(self, to: str, message: str) -> None: ...

class OrderService:                            # high level: pure business rules
    def __init__(self, repo: OrderRepository, notifier: Notifier):
        self.repo, self.notifier = repo, notifier

    def place(self, order: dict) -> str:
        if order["total_paise"] <= 0:
            raise ValueError("order total must be positive")
        order_id = self.repo.save(order)
        self.notifier.notify(order["email"], f"Order {order_id} placed")
        return order_id

# Low level: the details depend on the abstraction, never the other way around.
class InMemoryOrderRepository(OrderRepository):
    def __init__(self): self.saved: dict[str, dict] = {}
    def save(self, order: dict) -> str:
        oid = f"ord_{len(self.saved) + 1}"
        self.saved[oid] = order
        return oid

class RecordingNotifier(Notifier):
    def __init__(self): self.messages: list[tuple[str, str]] = []
    def notify(self, to: str, message: str) -> None: self.messages.append((to, message))


repo, notifier = InMemoryOrderRepository(), RecordingNotifier()
order_id = OrderService(repo, notifier).place({"total_paise": 49_900, "email": "a@mindforge.test"})

assert order_id == "ord_1" and repo.saved[order_id]["total_paise"] == 49_900
assert notifier.messages == [("a@mindforge.test", "Order ord_1 placed")]
print("business logic tested with zero infrastructure:", order_id)

The subtlety worth stating out loud in an interview: what actually "inverts" is the direction of the dependency. Before: OrderService → MySQLRepository. After: both point at OrderRepository instead — and critically, that interface belongs to the business layer and is written in business language (save(order)), not database language (executeQuery). That's what makes swapping Postgres for DynamoDB a change in exactly one file.

You've probably already used this without naming it. FastAPI's Depends() is Dependency Inversion, applied at the framework level:

Python
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/orders/{order_id}")
def read_order(order_id: int, db: Session = Depends(get_db)):
    return db.query(Order).get(order_id)

The endpoint function depends on whatever Depends(get_db) resolves to — a real database session in production, a test session in your test suite. Swapping one for the other never means touching the endpoint function at all. That's this whole principle, doing its job, and you've likely already relied on it without stopping to name it.

This is also the reasoning behind hexagonal ("ports and adapters") architecture, and it's exactly why the test above needed no database, no network call, and no mocking library at all.

Remember: the interface belongs to the business layer, written in its language — not to the database layer.

Sign in to try this knowledge check.

Quick recap

Principle, warning sign, fix:

Principle Warning sign Fix
S One reason to change A class three different teams keep editing; names like Manager/Util Split it up by reason to change
O Open to add, closed to edit A growing if/elif on a type Polymorphism, a strategy object, or a registry
L Children are safely swappable for parents An override that throws, or quietly weakens a guarantee Split the class tree; lean on composition instead
I No forced, unused dependencies "Not implemented" bodies, empty methods Small, focused interfaces
D Depend on abstractions Building infrastructure directly inside business logic Inject an interface the business layer owns
  • All five come down to one goal: making change cheap. When someone asks "why does SOLID even matter," answer with a change, not a definition: "adding a payment method should mean adding a class, not editing a switch statement."
  • Present them as fixes, not definitions. "Here's the broken version, here's what it breaks, here's the fix" is the format that actually scores points.
  • They lean on each other. An LSP break is usually an ISP problem underneath. OCP is usually achieved by applying DIP. SRP is what makes all the others even possible.
  • They can absolutely be overdone. Five one-method classes and an interface for every concrete type isn't SOLID — it's ceremony. Apply each principle where the requirements genuinely say something will change, and be ready to say out loud when you're deliberately choosing not to abstract something. That restraint is itself a senior-level signal.
PreviousOOP Foundations for DesignNextCreational Patterns

Course progress

0%
0 of 177 modules complete

On this page

  • S — Single Responsibility Principle
  • O — Open/Closed Principle
  • L — Liskov Substitution Principle
  • I — Interface Segregation Principle
  • D — Dependency Inversion Principle
  • Quick recap