This page is for anyone facing an object-oriented programming round, whether it sits inside a Java, C#, C++ or Python interview or stands alone. Most OOPs rounds start with the four pillars and class versus object, move to overloading, overriding, interfaces and composition, then test SOLID one principle at a time. Stronger rounds add design patterns, when not to use them, and a live class model such as a parking lot. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud. Swap in your own examples as you practise.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Encapsulation: keep data and the methods that change it together, and hide the internals.
Abstraction: show only what the caller needs, hide how it's done.
Inheritance: a new class reuses and extends an existing one.
Polymorphism: one call, different behaviour depending on the actual object.
"The four are encapsulation, abstraction, inheritance and polymorphism. Encapsulation is a bank account class where the balance is private and the only way to change it is deposit or withdraw, which check the rules. Abstraction is a car's pedals: I press the brake without knowing whether it's disc or drum brakes underneath. In code, that's a PaymentService with a pay method that hides which provider it calls. Inheritance is a SavingsAccount that reuses everything in Account and adds interest. Polymorphism is having a list of shapes and calling area on each one: a circle and a square each run their own version, and the loop doesn't care which is which. The way I remember it, encapsulation protects the data, abstraction hides complexity, inheritance reuses code, and polymorphism lets one piece of code work with many types."
Listing the four names with no examples, or giving the same example for encapsulation and abstraction without being able to say how they differ.
Class: the definition: which fields each object has and which methods it supports.
Object: one instance with its own values for those fields.
Memory: each object gets its own storage for instance fields; the method code is shared, not copied per object.
"A class is the definition. It says every Student has a name and a roll number and can do things like enroll in a course. An object is one actual student built from that definition, with its own values. If I create two Student objects, each has its own name and roll number, but they share the same method code; methods aren't copied into every object. In Java or C#, when I write new Student, the object is allocated on the heap and my variable only holds a reference to it, so assigning that variable to another one gives two references to the same object, not a copy. C++ also lets me create an object directly on the stack. So the class exists once, and there can be zero, one or a million objects of it at run time."
Saying each object carries its own copy of every method, or not knowing that object variables usually hold references.
Encapsulation: bundle state with the methods that guard it; stop outsiders changing it directly.
Abstraction: decide what idea to expose and hide the how behind it.
Together: abstraction picks the shape of the interface, encapsulation enforces it.
"I think of abstraction as a design decision about what to show, and encapsulation as the mechanism that protects what's hidden. Abstraction is choosing that a Notifier has one method, send, and callers never learn whether it goes by email or text message. Encapsulation is inside a class: the fields are private and every change goes through a method that keeps the object valid, like a withdraw method that refuses to take the balance below zero. You can have one without the other. A class with all public fields and one simple method has an abstraction but no encapsulation, because anyone can reach in and break it. So abstraction answers what does this thing do, and encapsulation answers who is allowed to touch its insides."
Saying they are the same thing, or defining encapsulation only as using private fields with no idea of protecting the object's rules.
Purpose: runs once when the object is created and puts it in a valid state.
Rules: in Java, C# and C++ it has the class name, no return type, and can be overloaded.
Defaults and chaining: a no-argument one is supplied only if you write none; one constructor can call another or the parent's.
"A constructor runs exactly once, when the object is created, and its job is to make the object valid from the first moment. If a BankAccount must always have an owner, I take the owner in the constructor, so there's no way to get an account without one. Unlike a normal method, in Java, C# or C++ it has the same name as the class and no return type, and I can't call it again on an existing object. I can overload it, say one version with just a name and another with a name and an opening balance, and have the shorter one call the longer one so the setup logic lives in one place. If I don't write any constructor, the compiler gives me a default one with no arguments, but as soon as I write one that takes arguments, that free default goes away. In Python the closest thing is __init__, which sets up an object that's already been created."
Describing the constructor as just a method that sets fields, with no idea of guaranteeing a valid object.
Purpose: control who can see and change a member, which protects the object's rules.
Default: start private, open up only when there's a real caller.
Protected and public: protected is a promise to subclasses, public a promise to everyone; both are hard to take back.
"Access modifiers control who can see and use a field or method. Private means only the class itself, protected adds subclasses, and in Java the same package too, and public means anyone. Languages add their own extras, like package-level access in Java or internal in C#, and Python only has naming conventions with underscores that aren't enforced. My rule is to start everything private and open it up only when there's a real need. Everything public is a promise: once other code depends on it, changing it breaks them. Protected is a promise too, to every subclass anyone ever writes, so I use it less than people expect. Fields stay private almost always, and I expose behaviour through methods that keep the object valid, rather than handing out raw access to the data."
Making fields public for convenience, or not being able to say why narrower visibility is safer.
Static: belongs to the class, one copy shared by all; a static method has no this.
Good uses: constants, pure helper functions, named factory methods.
Costs: mutable statics are global state: shared across threads, hard to reset in tests, hard to swap out.
"An instance field belongs to each object, so two orders have their own totals. A static field belongs to the class, so there's one copy shared by every object, and a static method runs without any particular object, which means it can't touch instance fields directly. I make something static when it genuinely doesn't depend on an object: constants, pure helpers like a function that converts units, or a named factory method like fromJson. The trouble starts with mutable static state. It's really a global variable: every thread shares it, one test can leave it dirty for the next, and code that calls a static method can't easily be given a fake version in tests. Also, in languages like Java, static methods aren't overridden polymorphically. So I keep statics stateless and pass real dependencies in as objects."
Making things static just to avoid creating an object, or not seeing that a mutable static field is shared global state.
Overloading: same method name, different parameter lists, picked by the compiler from the argument types.
Overriding: a subclass replaces an inherited method with the same signature, picked at run time from the real object.
Gotcha: a different return type alone is not a valid overload in Java, C# or C++.
"In a logging helper I wrote, I had log with just a message and log with a message and an exception. That's overloading: same name, different parameters, and the compiler picks which one to call from the arguments I pass, so it's decided at compile time. For overriding, I had a base Report class with a render method, and PdfReport and CsvReport each overrode it. The code held a Report reference and called render, and which version ran depended on the actual object at run time. That's dynamic dispatch, the heart of runtime polymorphism. One trap: changing only the return type doesn't make a valid overload. Also, Python doesn't overload by signature at all; a second method with the same name simply replaces the first, so there I'd use default arguments instead."
Swapping the two definitions, or claiming a method can be overloaded just by changing its return type.
Smell: the same switch on a type field repeated in several places.
Fix: give each type its own class with a common method, and let each class carry its own behaviour.
Payoff: adding a type means adding a class, not hunting down every switch.
"The smell is code like if channel equals email, do this; else if sms, do that; and the same chain copied into three other functions. Every new channel means editing all of them, and missing one is a bug. With polymorphism I define a common type, say a Notifier with a send method, and write one class per channel. The calling code just loops over notifiers and calls send; it never asks what type it has. Each class carries its own behaviour, so adding push notifications is one new class and nothing else changes. I'd still keep a single place, like a factory, that turns a config value into the right object, because some code has to make that choice once. But it's one place, not scattered everywhere."
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, user, text): ...
class EmailNotifier(Notifier):
def send(self, user, text):
print(f"email to {user}: {text}")
class SmsNotifier(Notifier):
def send(self, user, text):
print(f"sms to {user}: {text}")
def alert(notifiers, user, text):
for n in notifiers: # no type checks here
n.send(user, text)
alert([EmailNotifier(), SmsNotifier()], "asha", "Your order shipped")
Keeping the type checks inside the new classes, or not seeing that the win is adding a type without editing old code.
Abstract class: related types that share state and some implementation; can have fields and constructors.
Interface: a contract for a capability; a class can take on several; no instance state.
Common combo: interface as the public type, abstract base as an optional helper for implementers.
"I ask what I'm modelling. If I have a family of closely related types that share real state and code, like several report types that all have a title, an author and the same header logic, an abstract base class fits. It can hold fields, a constructor and finished methods, and leave just one or two abstract ones for each subclass. If instead I'm describing a capability that very different types might have, like being exportable or comparable, that's an interface. A class can implement several interfaces but usually extend only one base class, so interfaces keep my options open. Modern languages blur this a bit with default methods, but an interface still can't hold per-object state. In practice I often use both: callers depend on the interface, and an abstract class gives implementers a head start."
Answering only with syntax rules and never saying what kind of relationship each one models.
Problem: D inherits from B and C, which both inherit from A; which version of A's method, and how many copies of A's state?
Approaches: single class inheritance plus interfaces, virtual inheritance, or a fixed method resolution order.
Design lesson: deep multiple inheritance is usually a sign to use composition.
"Picture class A at the top, B and C both inheriting from it, and D inheriting from both B and C. That's the diamond. If B and C both override a method from A, which one does D get? And does D contain one copy of A's fields or two? Languages handle it differently. Java and C# only allow one base class, so the state problem can't happen; you can implement many interfaces, and in Java if two interfaces give conflicting default methods, the class has to override it and choose. C++ allows it; by default D really does get two copies of A, and you use virtual inheritance to share one. Python allows it and uses a fixed method resolution order so every class appears once and lookups are predictable. In my own designs, if I hit a diamond I usually take it as a hint to use composition instead."
Saying no language allows multiple inheritance, or being unable to describe the ambiguity itself.
Principle: no client should be forced to depend on methods it doesn't use.
Smell: implementers that throw not supported or leave empty bodies.
Fix: split by role, so each caller depends on the small interface it needs.
"The principle says a class shouldn't be forced to depend on methods it doesn't use. The classic smell is an interface like Machine with print, scan and fax. A cheap home printer has to implement all three, so scan and fax end up throwing not supported, and anyone calling fax on a Machine gets a surprise at run time. I'd split it into Printer, Scanner and Fax, each with its own methods. The office machine implements all three, the basic printer implements only Printer, and the print queue depends only on Printer. That makes each dependency honest, and changes to fax don't force recompiling or retesting code that only prints. I cut interfaces by who uses them, not by guessing, so I don't end up with a hundred one-method interfaces either."
Thinking the fix is to give the unused methods empty bodies, or splitting every interface into one method each by default.
Principle: a class should have one reason to change.
Test: list who might ask for a change; different people means different responsibilities.
Split: move each responsibility to its own class and let a coordinator use them.
"The principle says a class should have one reason to change. Take an Invoice class that calculates totals and tax, renders itself as a PDF, and emails itself to the customer. Three different people could ask for changes there: finance changes the tax rules, the design team changes the layout, and ops switches the email provider. Every one of those edits touches the same class and risks breaking the others. I'd keep Invoice for the data and the calculation rules, move PDF output to an InvoiceRenderer, and sending to an InvoiceMailer. Now a layout change can't break tax maths, and each piece is easy to test alone. It doesn't mean one method per class; a class can have many methods as long as they serve the same purpose."
Saying a class should do only one thing in the sense of one method, or giving an example with no reason to split.
Meaning: add new behaviour by adding code, not by editing code that already works.
How: put the part that varies behind an interface; new cases become new classes.
Limits: apply it where change is likely, not everywhere up front.
"It means that when a new requirement arrives, I should be able to add new code rather than reopen classes that are already tested and working. Say I have a report exporter with a switch for CSV and JSON. Every new format means editing that switch and retesting the whole thing. Instead I define an Exporter interface with an export method, and CSV and JSON are two classes behind it. When someone asks for Excel, I write an ExcelExporter and register it; the calling code and the existing exporters don't change at all. So it's open because new formats plug in, and closed because the core isn't touched. I don't do this for everything on day one, though. I usually wait until a second or third variation shows up, so I know which part actually changes."
Saying classes must never be edited, or adding interfaces everywhere in advance with no expected variation.
Principle: code written for the parent must still work correctly with any subclass.
The break: a rectangle promises width and height change independently; a square can't keep that promise.
Fix: don't relate them by inheritance, or make shapes immutable with a shared read-only type.
"Liskov says anywhere the code expects a Rectangle, I should be able to pass a subclass and nothing should break. In maths a square is a rectangle, so it feels natural to inherit. But a mutable Rectangle promises that setting the width leaves the height alone. A Square must keep both sides equal, so its setters change both. Now a function that sets width to five and height to four and expects an area of twenty gets sixteen from a square. The code compiles fine, every signature matches, and it's still wrong, because the subclass broke the behaviour the parent promised. The fix is to stop forcing the relationship: make them siblings under a read-only Shape with an area method, or make them immutable so there are no setters to break. The lesson is that is-a in real life doesn't guarantee is-a 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
def set_height(self, h): self.w = self.h = h
def stretch(r: Rectangle):
r.set_width(5)
r.set_height(4)
return r.area() # 20 for Rectangle, 16 for Square
Saying the design is fine because a square is mathematically a rectangle, or thinking matching signatures is enough to satisfy Liskov.
Principle: high-level policy and low-level details both depend on an abstraction; the abstraction belongs to the high-level side.
Injection: a technique: an object receives its dependencies, often through the constructor, instead of creating them.
Relationship: injection helps you apply the principle, but injecting concrete classes doesn't achieve it.
"Dependency inversion says my business logic shouldn't depend on low-level details like a specific database or payment provider. Both should depend on an abstraction, and that abstraction is shaped by what the business logic needs. So an OrderService depends on a PaymentGateway interface that the order module defines, and the concrete provider class implements it. The arrow that used to point from orders down to the provider now points from the provider up to the interface; that's the inversion. Dependency injection is a technique: instead of the OrderService creating its own gateway with new, someone passes one into its constructor. Injection makes the principle easy to apply, but they aren't the same. I can inject a concrete provider class and still be tightly bound to it, and I can follow the principle with a simple factory and no framework at all."
Treating the principle, dependency injection and a framework's container as one and the same thing.
Meaning: build behaviour by holding and delegating to other objects rather than extending a class.
Why: inheritance ties you to the parent's internals and fixes behaviour at compile time.
When inheritance is fine: a true is-a relationship where the child can stand in for the parent everywhere.
"It means that when I want to reuse behaviour, my first choice is to have an object hold another object and delegate to it, rather than extend its class. Inheritance couples a subclass to the parent's implementation, so a change in the parent can quietly break every child, and the behaviour is fixed when I write the class. A well-known case is Java's own Stack class, which extends Vector, so a stack lets you insert into the middle, which no stack should allow. Holding a list inside and exposing only push and pop would have been right. Another sign is class explosion: FlyingDuck, SwimmingDuck, FlyingSwimmingDuck. With composition I give a duck a fly behaviour and a swim behaviour and can swap them at run time. Inheritance is still fine when the child truly is a kind of parent and can replace it anywhere."
Saying inheritance should never be used, or being unable to name any real cost of it.
Cohesion: how closely the things inside one class belong together.
Coupling: how much one class knows about and relies on another.
Goal: focused classes that talk through small interfaces, so a change stays local.
"Cohesion is about what's inside a class: do all its fields and methods serve one purpose? A ShoppingCart that adds items, removes them and totals them is cohesive. If it also sends marketing emails, cohesion drops. Coupling is about the links between classes: how much one knows about another's internals. If my checkout code reaches into the cart's private list and the product's pricing fields, it's tightly coupled, and changing either one breaks checkout. We want high cohesion because a focused class is easier to understand and test, and low coupling because then a change in one class stays inside it instead of rippling across the codebase. In practice I lower coupling by talking through small interfaces and asking objects to do things rather than pulling their data out."
Mixing the two up, or saying both should be as high as possible.
Association: one object knows about or uses another; no ownership.
Aggregation: a whole-part link where the part can live on without the whole.
Composition: strong ownership; the part is created with the whole and dies with it.
"All three are has-a relationships; the difference is ownership and lifetime. Association is the loosest: a Doctor treats Patients, a Teacher teaches Students. They know about each other, but neither owns the other. Aggregation is a whole-part link where the part can exist on its own: a Team has Players, but if the team is dissolved, the players still exist and can join another team. Composition is strong ownership: an Order has OrderLines, and a line makes no sense without its order. If the order is deleted, its lines go with it, and nobody else holds them. In UML aggregation is a hollow diamond and composition a filled one. In code, it shows up in who creates the part, whether it's shared, and what gets deleted together."
Treating aggregation and composition as the same, or not mentioning the lifetime of the part.
What it is: a class that ensures only one instance exists and offers a global way to reach it.
Fair uses: something truly shared and expensive: a connection pool, a config loaded once.
Costs: hidden dependencies, global mutable state, hard to replace in tests, thread-safety to get right.
"A singleton guarantees one instance and gives everyone a global way to reach it. Having only one of something is often fine: one connection pool, one config loaded at startup. The trouble is the global access. Any class can grab it without saying so in its constructor, so dependencies are hidden. If it holds mutable state, it's a global variable in disguise, shared by every thread and every test. And because callers fetch it themselves, I can't easily swap in a fake for a unit test. So what I usually do is create one instance at startup and pass it to the classes that need it. I still get one instance, but the dependency is visible and replaceable. I'd only use a classic singleton for something that holds no business state, like a logger, where those costs barely matter."
Reaching for a singleton whenever something is shared, or not being able to name a single downside.
Problem: callers shouldn't need to know which concrete class to build or how to build it.
Shape: one method takes input or config and returns an object behind a common type.
Skip it: when there's one concrete class and building it is simple.
"A factory puts the decision about which class to create in one place. Say I read a file and the extension decides whether I need a CsvParser, a JsonParser or an XmlParser. Without a factory, that if-else ends up in every caller, and each caller now depends on all three classes. With a ParserFactory, callers ask for a parser for this file and get back something of type Parser; they never see the concrete classes. It also helps when building an object takes several steps or needs config the caller shouldn't know about. But if there's only one implementation and the constructor takes two simple arguments, a factory is just extra code to read. I'd call new directly and add the factory the day a second variation actually appears."
Wrapping every object creation in a factory by default, or not being able to say what decision the factory is hiding.
Idea: pull a varying algorithm out into its own family of classes behind one interface.
Wiring: the context holds a strategy and calls it; it doesn't know which one it has.
Extending: a new rule is a new class; the context is unchanged.
"Strategy is for when one step of a process has several interchangeable versions. At checkout, the discount rule varies: none, a seasonal cut, a member rate. Instead of an if-else inside Checkout, I define a DiscountRule interface with one apply method and write each rule as its own small class. Checkout takes a rule in its constructor and just calls apply; it has no idea which rule it has. Adding a new promotion later means writing one new class and choosing it where Checkout is created, usually from config. Checkout and the existing rules don't change, and each rule can be unit tested alone. In languages with first-class functions I'd often just pass a function, which is the same idea with less code."
interface DiscountRule {
long apply(long totalCents);
}
class NoDiscount implements DiscountRule {
public long apply(long totalCents) { return totalCents; }
}
class SeasonalDiscount implements DiscountRule {
public long apply(long totalCents) { return totalCents - totalCents / 10; }
}
class Checkout {
private final DiscountRule rule;
Checkout(DiscountRule rule) { this.rule = rule; }
long finalTotal(long totalCents) { return rule.apply(totalCents); }
}
Keeping the if-else inside the context and calling it strategy, or not knowing where the choice of strategy is made.
Mechanics: a subject keeps a list of observers and notifies each when something changes.
Benefit: the subject doesn't know who is listening, so new reactions plug in freely.
Pitfalls: listeners never removed, order assumptions, one failing observer, cascades that are hard to follow.
"A subject, say an Order, keeps a list of observers. When its status changes it loops through the list and calls each one's update method. The email sender, the stock updater and the analytics tracker all subscribe, and the Order doesn't know any of them exist, so adding a new reaction is just one more subscriber. In a long-running app, the classic problem is forgetting to unsubscribe. The subject still holds a reference, so an old screen or object can't be garbage collected and keeps receiving events: a memory leak plus strange behaviour. Others: observers quietly relying on notification order, one observer throwing and stopping the rest from being called, and chains where one update triggers another until nobody can trace why something happened. I handle those with explicit unsubscribe on teardown, isolating failures per observer, and logging events."
Describing the mechanics correctly but having never thought about unsubscribing or what happens when an observer throws.
Clarify: floors, spot sizes, vehicle types, tickets, how fees are charged.
Core classes: ParkingLot, Level, ParkingSpot, Vehicle, Ticket, with clear ownership.
Variable rules: spot allocation and fee calculation as swappable strategies.
Edge cases: lot full, lost ticket, two cars grabbing the same spot.
"First I'd ask a few questions: how many levels, which vehicle types, do spot sizes matter, and is pricing hourly or flat. Assuming levels, three spot sizes and hourly fees, I'd have a ParkingLot that owns Levels, and each Level owns ParkingSpots. A spot knows its size and whether it's free. A Vehicle has a plate and a size; I'd use an enum for size rather than a subclass per vehicle, because the vehicle types don't behave differently here. When a car enters, the lot asks a SpotAllocator for a suitable free spot and creates a Ticket holding the vehicle, spot and entry time. On exit, a FeeCalculator turns the ticket into a charge. I'd keep allocation and pricing as interfaces so a nearest-spot rule or weekend rate can be swapped in. And I'd make claiming a spot safe when two entry gates try at once."
Jumping straight into a big inheritance tree of vehicle subclasses without asking any requirements or saying where responsibilities live.
Title vs copy: a Book holds shared details; each BookCopy is one physical item with its own status.
Loan: a class of its own linking a copy, a member and dates.
Rules: limits and due dates live in a policy or service, not scattered across Book and Member.
"The first thing I'd separate is the book from its copies. A Book is the title, author and ISBN; a BookCopy is one physical item on the shelf with its own barcode and status, available, on loan or lost. The library can own five copies of one book, and you borrow a copy, not a title. Then a Member with an id and a membership type, and a Loan that links one copy to one member with a borrow date, a due date and a return date. Loans deserve their own class because they have their own data and history. The rules, like how many books a member can hold or how long a loan lasts, I'd put in a LoanPolicy that a LibraryService checks before creating a loan. That keeps Book and Member simple and lets rules differ by membership type."
Treating a book and a copy as the same object, or loading every rule into one giant Library class.
Situation: how deep the tree was and what pain it caused.
Change: what you pulled out into composed pieces or interfaces.
Safety: tests first, small steps, nothing big-bang.
Result: what got easier afterwards.
"At my last company we had a notification module where BaseNotification had grown into five levels of subclasses: EmailNotification, then ScheduledEmailNotification, then RetryingScheduledEmailNotification, and so on. Adding a retrying SMS meant copying half a branch, and a change in the base class broke three channels at once. I first wrote tests around the current behaviour for each channel. Then I pulled out the things that varied, the channel, the scheduling and the retry policy, into small objects behind interfaces, and made a single Notification class that holds one of each. I moved one channel at a time, keeping the old classes until the new path passed the same tests. At the end we had one class and a handful of small parts, and adding a new channel became a single class."
A story of a big-bang rewrite with no tests, or one where the new design is just a different deep hierarchy.
What you built: the pattern and why it felt right then.
What went wrong: the cost it added with no real benefit.
What you did: how you simplified it.
Rule now: how you decide today.
"Early in my career I built an internal reporting tool and was keen on patterns. For exporting reports I added an Exporter interface, an abstract base exporter, a factory, and a registry that loaded exporters by name from config. We only ever had one format, CSV. For a year, every small change meant reading four files to find the one line that mattered, and a new teammate asked me why it was so complicated. I couldn't give a good answer. I collapsed it into a single CsvExporter class, which cut the code by more than half and made the bug fixes trivial. When we did later add Excel, pulling out an interface took an afternoon. My rule now is to add the abstraction when the second real case arrives, not when I imagine it might."
Claiming you've never over-engineered anything, or telling the story without a lesson you still use.
Overview: what the project does in one line.
Main classes: three to five, with what each is responsible for.
Change now: one concrete decision you'd revisit and why.
"In my final-year project I built a small hospital appointment system. The main classes were Patient, Doctor, Appointment, and a Schedule that belonged to each doctor and held their free slots. An AppointmentService took a request, asked the doctor's schedule for a free slot, and created the Appointment. Notifications went through a separate Notifier class so the booking logic didn't know about email. What I'd change now: I put the cancellation rules, like no cancelling within two hours, directly inside the Appointment class with a bunch of if statements, and every new rule meant editing it. Today I'd pull those into a small CancellationPolicy object, so the rules could differ by department and be tested on their own without building whole appointments."
Describing screens and database tables only, with no classes or responsibilities, or claiming there is nothing you'd change.
Ask first: is a second implementation actually planned, or is this a guess?
Cost: more files and indirection for every reader, today, for a benefit that may never come.
Tone: suggest the simpler version, explain how easy it is to add the layers later.
"I'd start by asking, not ruling. Maybe there's a second implementation on the roadmap I don't know about. If there is, and it's soon, the design might be right. If it's a just-in-case, I'd explain the cost in the review: three extra types and a factory that every reader has to click through, to reach one class that does the work. I'd suggest shipping the single concrete class and point out that extracting an interface later, when a real second case shows up, is a quick and safe refactor. I'd keep it about the code, not about their skills, and say what they got right, like keeping the class small and testable. If they still feel strongly and it's not harmful, I'd let it go rather than block the release over style."
Approving without comment to avoid friction, or rejecting the change as over-engineered without asking why it was built that way.
Name the risk: any code holding the parent type can now crash at run time.
Root cause: the parent promises more than every child can do.
Options: a narrower interface, a separate class, or a clearly tracked short-term exception.
"I'd explain the risk in concrete terms: every piece of code that holds the parent type assumes all its methods work. With this subclass, some of them will blow up at run time, and nothing at compile time will warn anyone. It's a sign the parent is promising too much. So I'd look for the smallest fix that still fits the deadline. Often that's splitting out a narrower interface with just the methods the new class really supports, and having the callers that need it depend on that instead. If that's too big for now, I'd suggest making it a separate class that doesn't inherit at all. If we truly must ship it this way, I'd want it documented, kept away from generic code paths, and a ticket to fix it right after the release."
Saying it's fine as long as the new methods throw clear errors, or blocking the deadline without offering any alternative.
Protect: write characterization tests around the part you'll touch.
Isolate: extract the area you need into a new, small class behind the old method.
Add: build the feature in the new class, with proper tests.
Limit scope: no rewrite; leave it a little better, not perfect.
"I wouldn't try to rewrite it, and I wouldn't just add another two hundred lines either. First I'd find the exact methods my feature touches and write characterization tests around them: tests that record what the code does today, even the odd parts, so I'll know if I change it by accident. Then I'd extract the logic I need into a new, focused class, and have the old methods delegate to it, so every caller still works unchanged. My feature goes into the new class, where it's small enough to test properly. If the class creates its own dependencies, I'd add a way to pass them in so the tests don't hit a real database. I'd keep each step a separate small commit, and I'd tell the team what I extracted so the next person can keep going the same way."
Proposing a full rewrite before the feature, or adding the change straight into the big class with no tests at all.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.