Low-Level Design & Design Patterns Interview Questions
SOLID principles, all 23 Gang-of-Four design patterns, and a capstone system-design walkthrough -- learned from real, motivating problems, not memorized definitions.
← Learn this topic from scratch firstStrategy Pattern
How is the Strategy pattern specifically a form of the Open/Closed Principle?
intermediateThe Open/Closed Principle says code should be open for extension but closed for modification, and Strategy delivers exactly that for swappable behavior. Once a context class delegates to an interface like PaymentStrategy instead of branching on a type string internally, adding a brand-new algorithm — a new payment method, a new routing option, a new validation rule — means writing one new class that implements the shared interface. The context class itself, along with everything that already depends on it and everything that's already been tested against it, does not change by a single line. That's the concrete, checkable proof of the principle in action: extension happens by addition, not by editing code that already works.
Strategy and State look structurally almost identical. What's the actual difference between them?
intermediateBoth patterns involve a context class holding a reference to an interchangeable object and delegating to it, so a class diagram alone often can't distinguish them. The real difference is intent and who controls the switch. With Strategy, the caller explicitly chooses and sets which implementation to use — a PaymentService doesn't decide on its own to switch from credit card to UPI, some external code calls setPaymentStrategy() to make that choice. With State, the object's own internal logic decides when to transition, often without any external caller ever calling a setter — a vending machine's own state object might trigger a transition to a different state as a direct consequence of handling a request, with the caller having no idea a switch even happened.
Can you give a concrete example of the Strategy pattern already built into the Java standard library?
intermediatejava.util.Comparator, used with List.sort(Comparator) or Collections.sort(list, comparator), is a textbook example. The sorting algorithm behind the scenes stays completely fixed — the JDK doesn't rewrite its sort implementation for every different ordering you might want. What varies is the comparison logic that decides which of two elements should come first, and that logic is fully swappable: pass a comparator that orders by name, pass a different one that orders by date, and the same sort() call produces different results without any change to the sorting algorithm itself. This is exactly the Strategy shape — a fixed context (the sort operation) delegating to an interchangeable, caller-supplied algorithm (the comparison logic) behind a single-method interface.
Liskov Substitution Principle
Why does the classic 'Square extends Rectangle' design violate the Liskov Substitution Principle?
intermediateIf Rectangle exposes independent setWidth() and setHeight() methods, a caller can reasonably assume setting one doesn't affect the other. Square, to keep its sides equal, must override both setters so that changing width also changes height and vice versa — which silently breaks that assumption for any code written against Rectangle. A test that creates a Rectangle, sets width to 5 and height to 10, and asserts the area is 50 will fail if it's secretly handed a Square instance, even though Square passes every structural check a Rectangle requires. The deeper lesson is that a real-world "is-a" relationship, which is mathematically true here, doesn't automatically translate into safe behavioral substitutability in code, since substitutability depends on preserved behavior, not just a valid conceptual category.
What is the difference between structural compatibility and behavioral compatibility when substituting a subclass for its parent?
intermediateStructural compatibility means a subclass has all the same method signatures as its parent, so the code compiles when the subclass is used anywhere the parent is expected. Behavioral compatibility means the subclass actually honors the meaning and guarantees behind those methods — the same preconditions, postconditions, and side effects a caller could rely on from the parent. A class like ReadOnlyFile extending File and overriding write() to throw an exception is structurally compatible, since it satisfies the compiler completely, but it is not behaviorally compatible, because callers were implicitly promised that write() was safe to call. LSP specifically requires behavioral compatibility, which is why it is described as a semantic principle rather than a purely structural one — passing the compiler is necessary but never sufficient for a subclass to be a genuinely safe substitute.
What is a real example from the Java standard library where a class technically satisfies an interface but violates LSP?
intermediateList.of(...) and Arrays.asList(...) both return List implementations that are fixed-size or immutable, yet they implement the exact same java.util.List interface that mutable implementations like ArrayList implement. Calling .add(), .remove(), or .set() on these lists compiles perfectly, since List declares those methods, but throws UnsupportedOperationException at runtime instead of actually performing the mutation. Any method written generically against List, trusting that add() is always safe because the interface declares it, can be handed one of these immutable lists and crash at runtime with no compile-time warning at all. This is a documented, intentional trade-off in the JDK for memory efficiency and safety around accidental mutation, but it remains the textbook real-world illustration of an LSP violation, since it is exactly the same shape as a ReadOnlyFile throwing from write().
Abstract Factory Pattern
What does Abstract Factory guarantee that using several independent Factory objects cannot?
intermediateIt guarantees that every product created from a single factory instance belongs to the same consistent family, which several separate factories, one for buttons and a different one for scroll bars, can never enforce on their own. With independent factories, nothing stops code from asking the button factory for a Windows button and the scroll-bar factory for a Mac scroll bar, producing a visually and logically inconsistent result that compiles and runs without error. Abstract Factory closes this gap by defining one interface with a creation method for every product in the family, so that a single concrete factory, such as WindowsFactory, is the only source for all of that family's related objects, making a mismatched combination structurally impossible rather than merely discouraged.
How does Abstract Factory make it structurally impossible to end up with a mismatched pair of related objects?
intermediateEach concrete factory, like WindowsFactory, only ever references its own family's concrete classes inside its own source code — its createScrollBar() method returns a WindowsScrollBar, and the MacScrollBar class is not mentioned anywhere inside it at all. Because client code receives one factory instance and asks that same instance for every product it needs, there is no code path available that could construct a button from one factory and a scroll bar from a different, mismatched one. The guarantee is not a coding convention someone has to remember to follow — it is a direct consequence of which concrete factory class the client happens to be holding a reference to.
What is the real cost of adding a new product to an existing Abstract Factory family, compared to adding a new family such as a new theme?
intermediateAdding a new product, say a Checkbox alongside Button and ScrollBar, means adding a new createCheckbox() method to the shared factory interface, and every existing concrete factory implementing that interface, WindowsFactory and MacFactory and any others, must now implement that new method too, or the code fails to compile. This is a real, non-trivial cost that grows with however many concrete factories already exist. Adding an entirely new family instead, a LinuxFactory say, costs nothing extra to existing factories at all: it is simply one new class implementing the existing interface, with zero changes required anywhere else. Abstract Factory is cheap to extend with new families and comparatively expensive to extend with new products, which is worth knowing before choosing it.
Flyweight Pattern
What is the difference between intrinsic and extrinsic state in the Flyweight pattern, and why does that distinction matter for memory usage?
advancedIntrinsic state is the part of an object's data that is identical across many instances -- for example, a bullet's color or its rendered image -- and it can safely be extracted into one shared object stored exactly once. Extrinsic state is the part that is genuinely unique to each individual instance, such as a bullet's x and y position, and it must stay with each object individually since it cannot be shared. The distinction matters because Flyweight's entire memory saving comes from paying the cost of intrinsic state once per unique value instead of once per object, while extrinsic state stays cheap because it is usually just a few primitive fields. Get the split wrong -- for example, sharing something that should have been per-instance -- and objects that should behave independently will start silently affecting each other.
When is the Flyweight pattern actually worth applying, and what makes it risky to reach for by default?
advancedFlyweight is worth applying only when there is a genuine, measured memory problem coming from a very large number of objects that share substantial identical state -- the kind of situation where doing the actual per-object byte math shows a real, significant cost. Applying it without that measured justification adds real complexity for no benefit: a factory managing a shared cache, and a mandatory split between shared and per-instance state that has to be maintained correctly everywhere the object is used. It is one of the most niche patterns available, and the risk of over-applying it is genuinely higher than the risk of missing an opportunity to use it, since most object counts in ordinary applications never get large enough for the savings to matter.
Why must a shared flyweight object be immutable once it has been constructed?
advancedA flyweight instance is deliberately referenced by many different objects at once -- that sharing is the entire point of the pattern. If any code were able to mutate a flyweight's fields after construction, every single object holding a reference to that shared instance would change at the same moment, silently, with no indication at the mutation's call site that it was affecting anything beyond the one object being touched directly. That makes immutability a hard requirement rather than a nice-to-have: without it, sharing state to save memory introduces a correctness bug that is extremely difficult to trace back to its source, since the bug manifests far away from where the actual mutation happened.
Command Pattern
In the Command pattern, what's the difference between the Invoker and the Receiver, and why does that split matter?
intermediateThe Invoker is the object that triggers a command — a button, a menu item, a scheduled task runner — and its only job is calling execute() on whatever command it's currently holding, with zero knowledge of what that command actually does. The Receiver is the object that holds the real logic and actually performs the work, like a TextEditor's makeBold() method. The Command implementation sits between them, bound to one specific receiver method. This split matters because it lets the Invoker be written once and reused for literally any action: a GUI framework's Button class never needs to know about TextEditor, PhotoEditor, or any other receiver, because it only ever depends on the shared Command interface, not on any concrete receiver type.
The basic Command interface only has an execute() method. What has to change to support undo?
intermediateThe Command interface needs a second method, typically undo(), that every concrete command also implements. More importantly, a concrete command usually has to capture whatever state is needed to reverse itself at the moment execute() actually runs, not afterward when undo() is eventually called, because the state needed to reverse the action may no longer be available or correct by that later point. For a bold-toggling command, that might mean recording whether the text was already bold before execute() ran, so undo() can restore that exact prior state rather than guessing. This is structurally the same problem the Memento pattern solves — capturing state before a change so it can be restored later — and the two patterns are frequently combined: a command's undo() implementation stores or uses a memento of the receiver's state.
How does java.lang.Runnable relate to the Command pattern?
intermediateRunnable is structurally a Command: it's a functional interface with a single method, run(), that takes no parameters and returns nothing — the exact same shape as a Command interface's execute() method. When code hands a Runnable to a Thread constructor or submits it to an ExecutorService, it's doing precisely what an Invoker does with a Command: holding a self-contained, executable unit of work without needing to know what that work actually involves. The executor framework itself acts as the Invoker, queueing and eventually calling run() on each submitted Runnable, completely decoupled from the specific logic each one contains — which is exactly why task queues and thread pools are one of the most common real-world applications of this pattern.
Proxy Pattern
How does a virtual proxy implement lazy loading, and what determines when the real object actually gets created?
intermediateA virtual proxy implements the same interface as the real, expensive object, but its constructor deliberately does none of the expensive work -- it just stores enough information, like a file name or an identifier, to construct the real object later. It holds a field for the real object that starts out null, which represents 'not created yet.' Every method the client calls on the proxy first checks that field: if it's still null, the proxy constructs the real object right there, triggering the expensive work for the first time, and stores the result; if it's already set, the proxy skips straight to delegating the call. This means the real object gets created exactly once, on the first genuine use, and never at all if the client never actually needs it.
What thread-safety bug can a naive lazy-loading proxy have, and how would you fix it?
intermediateA naive lazy-loading proxy checks 'if the real object reference is null, construct it' without any synchronization. Under concurrent access, two threads can both read the field as null before either one finishes constructing the real object, so both threads construct their own separate instance -- doubling the expensive work and breaking the single-instance caching guarantee the proxy was supposed to provide. The fix is to make the check-and-create sequence atomic, either by synchronizing the entire method, which is simple but serializes every call, or by using double-checked locking with a volatile field, which only pays the synchronization cost on the first call. Which fix makes sense depends on how much concurrent contention the proxy actually expects to see.
How do ORM frameworks like Hibernate use the Proxy pattern for lazy loading of related entities?
intermediateWhen an entity has a relationship configured for lazy loading, Hibernate doesn't return the actual related entity when the parent object is loaded -- it returns a runtime-generated proxy object that implements the same type as the real entity but holds only its identifier. The proxy doesn't query the database at construction time. Only when a genuine field on that related entity is accessed does the proxy fire the actual query, fetch the real data, and delegate to it from then on. This lets an application load a parent object cheaply without pulling in every related entity up front, while code that accesses the relationship still works exactly as if it were holding the real entity the whole time -- the same lazy-loading and access-control ideas as the virtual proxy pattern, applied to database access.
Factory Method Pattern
What specific problem does the Factory pattern solve that plain object construction does not?
intermediateIt centralizes the decision of which concrete class to instantiate into exactly one place, instead of letting that decision be duplicated across every piece of client code that needs an object. Without a factory, if ten different features in an application each need to construct a Car or a Bike depending on some condition, that same decision logic is copy-pasted into ten places, and adding a new type means finding and updating every one of them. With a factory method, client code asks for what it wants, such as a car or a bike, and never writes new on a concrete class itself, so a new type can be added by touching only the factory and the new class, not any of the client code that requests objects.
Why should a factory method throw an exception for an unrecognized type instead of returning null?
intermediateReturning null for an unrecognized type defers the failure to whatever code eventually tries to use the returned object, which typically surfaces as a NullPointerException somewhere far from where the actual mistake, an unsupported type string, was made. Throwing immediately, with a message that names the specific unsupported type, fails loudly and precisely at the actual point of the problem, which is dramatically easier to debug. The general principle is to fail as close to the actual bad input as possible, rather than letting a bad value propagate silently until it causes a confusing symptom somewhere else in the call stack.
When is introducing a Factory pattern unnecessary, and what is the risk of overusing it?
intermediateA Factory earns its keep specifically when which concrete class to construct depends on some runtime condition, and that decision needs to be made consistently from more than one place in the codebase. If an application only ever constructs one concrete type, from exactly one place, a factory is pure unnecessary indirection — it adds a layer of abstraction with no corresponding benefit. Overusing it also has a real cost: even inside a legitimate factory, a switch statement handling many types can itself become unwieldy as the type count grows, though at least that growth is confined to one sanctioned location; for a very large number of types, some codebases replace the switch with a map from type identifier to constructor function to avoid editing the factory's method body for every new addition.
Template Method Pattern
Why is the orchestrating method in a Template Method typically declared final, and what breaks if it isn't?
intermediateMarking the orchestrating method final is what actually enforces the pattern's entire purpose: a fixed sequence of steps that subclasses can customize in one specific place but never reorder or skip. If that method is left non-final, any subclass can simply override it wholesale, and nothing in the language stops that override from calling the shared steps in the wrong order, skipping one entirely, or adding unrelated logic between them. That silently defeats the guarantee the pattern exists to provide — a resource-cleanup step like closeFile() could be skipped by a careless override, reintroducing exactly the bug the pattern was built to prevent. The final keyword is not a stylistic choice here; it is the mechanism.
What's the difference between an abstract method and a hook method in the Template Method pattern?
intermediateAn abstract method has no implementation on the base class at all, and the language forces every concrete subclass to provide one before it can even be instantiated — this is used for steps that are mandatory and genuinely different per subclass, like the actual parsing logic in a file parser. A hook method has a real, usually minimal or empty, default implementation on the base class, and a subclass is free to override it if it needs different behavior, but isn't required to. Hooks are the right tool for optional steps — an afterParse() cleanup step that only some formats need, for instance — where forcing every subclass to implement something would just mean most of them writing an empty method body out of obligation.
Most behavioral patterns favor composition over inheritance. Why does Template Method deliberately do the opposite?
intermediateComposition is usually preferred because it keeps classes loosely coupled and lets behavior be swapped at runtime, but Template Method's entire goal is the opposite of swappable — it needs to guarantee that a fixed sequence of steps can never be rearranged by anything using it, while still allowing exactly one specific step to vary. Inheritance, paired with a final orchestrating method and a protected abstract or hook method for the varying step, directly enforces that constraint at compile time: a subclass literally cannot override the sequencing method, and it must (for abstract steps) or may (for hooks) supply its own version of the customizable step. Composition could not enforce that same guarantee nearly as directly, since a composed strategy object could always be called in the wrong order by whatever holds it. This is a case where inheritance's tighter coupling is genuinely the right tool for the job rather than a design smell.
Interface Segregation Principle
What is a common code smell that signals an interface is violating the Interface Segregation Principle?
intermediateThe clearest warning sign is a method implementation whose entire body is throwing UnsupportedOperationException, returning null unexpectedly, or doing nothing at all — a class technically satisfies the interface's compile-time contract but genuinely can't provide the behavior. Another sign is a large interface where different implementers each leave a different subset of methods unimplemented or stubbed, which suggests the interface is really several unrelated capabilities bundled together rather than one coherent contract. A third sign is defensive calling code — try/catch blocks wrapped around interface method calls specifically because some implementations are known to not really support them — which shows the interface's promise can't actually be trusted at the type level. The fix in every case is the same: split the interface along the capabilities that genuinely vary between implementers, so each implementer only commits to what it can honestly deliver.
How is the Interface Segregation Principle different from the Single Responsibility Principle?
intermediateSRP is about a class having exactly one reason to change — it's a rule about how a class's own responsibilities should be scoped. ISP is about an interface not forcing the classes that implement it to depend on methods they don't actually use — it's a rule about how a contract's shape affects every implementer of that contract. A class can perfectly satisfy SRP while still being harmed by an ISP violation: a SimplePrinter class with the single, focused job of printing can still be forced to implement scan() and copy() methods it has no use for, if the Machine interface it implements bundles unrelated capabilities together. In short, SRP is scoped to a class's own job, while ISP is scoped to how well an interface's shape matches what each of its implementers can genuinely support.
Can you over-apply the Interface Segregation Principle? What's the downside of splitting interfaces too finely?
intermediateYes — splitting every single method of a coherent interface into its own separate single-method interface, regardless of whether those methods ever vary independently across implementers, is a real over-application of ISP. The downside is added indirection without a matching benefit: a class that needs to do one coherent job now has to declare eight tiny interfaces in its implements clause instead of one interface with eight related methods, which makes the code harder to read and navigate for no real gain. The right granularity is to split along capabilities that genuinely vary independently across real implementers — for example, some machines print but cannot scan — rather than splitting simply because each method is technically separable in isolation. A useful check is whether any real, expected implementer would ever want one method from a proposed interface without the others; if the answer is consistently no, those methods likely belong together.
Singleton Pattern
What are the essential building blocks of a Singleton implementation in Java, and why does each one matter?
beginnerA Singleton needs three pieces: a private static field to hold the single instance, a private constructor so no code outside the class can call new on it, and a public static method that lazily creates the instance on the first call and returns that same instance on every call after. The private constructor is what actually enforces exactly one instance — without it, any code anywhere could construct additional objects regardless of what the static method does. The static field and method have to be static specifically because there may be no instance yet when a caller first asks for one, so the method has to be callable on the class itself rather than on an object. Get any one of the three wrong — a public constructor, an instance method as the accessor, or forgetting to check for an existing instance — and the exactly-one guarantee silently breaks.
Why does the simple, lazily-initialized Singleton break under concurrency, and how does double-checked locking fix it?
intermediateIf two threads call the accessor method at nearly the same time, both can read the instance field as null before either one has finished constructing an object, and both proceed to build and assign their own separate instance, violating the entire point of the pattern. Double-checked locking fixes this by checking for null twice: an outer check, unsynchronized, so the common case of the instance already existing skips locking entirely; and an inner check, taken only after acquiring a lock, which prevents two threads that both passed the outer check simultaneously from each constructing a separate object. The instance field also has to be declared volatile, because without it a JVM optimization called instruction reordering could let one thread observe a half-constructed object. A simpler alternative that sidesteps all of this manual locking is an enum-based Singleton, which the JVM guarantees is thread-safe automatically.
What is the core criticism of the Singleton pattern, and what would a better alternative look like in a Spring application?
beginnerA Singleton is really just global, shared mutable state given a more respectable name, and it inherits all of global state's usual problems — it is hard to unit test, since there is no way to swap in a fake instance for a test when access is hardcoded to one static method, and it hides a class's real dependencies, since any code can silently reach for getInstance() without that dependency ever showing up in a constructor signature. It also directly works against dependency inversion, since code ends up depending on one concrete, globally accessible class instead of an injected abstraction. In a Spring Boot application, a @Service or @Component class is a singleton-scoped bean by default, meaning the framework guarantees exactly one shared instance, but that instance is injected wherever it is needed rather than fetched through a global static method — giving you the exactly-one-instance benefit without the hidden-dependency, hard-to-test cost of a hand-rolled Singleton.
Iterator Pattern
Why does exposing a collection's raw backing structure to client code create a real maintenance risk?
intermediateThe moment client code calls a method like get(i) or relies on any other structure-specific operation, that code becomes tightly coupled to exactly one implementation detail — that the collection happens to be backed by something indexable, like a List. If the collection's author later switches the backing storage to something without that operation, such as a TreeSet or a HashSet, every single piece of client code written against the old structure stops compiling or behaves incorrectly, even though the client never should have needed to know or care what the internal storage was in the first place. The Iterator pattern fixes this by giving client code a uniform hasNext()/next() contract instead, so the collection's author can change internal storage freely as long as the iterator's own logic is updated to match — client code using that contract never has to change.
Why does an iterator's current position live on the iterator object itself, rather than on the collection being traversed?
intermediateIf the current traversal position were stored as a field on the collection instead of on a separate iterator object, only one traversal could ever be in progress at a time — a second caller starting to iterate would either interfere with the first caller's position or be forced to wait. By keeping position as a field on a fresh iterator object returned each time traversal starts, any number of independent callers can iterate over the exact same collection simultaneously, each with its own private notion of where it currently is, without any of them stepping on each other. This is exactly why methods like createIterator() or Iterable's iterator() return a new object on every call rather than a single shared instance.
What actually happens under the hood when you write a for-each loop over a Java collection?
intermediateA for-each loop like for (Book b : collection) is syntactic sugar that the Java compiler rewrites directly into Iterator pattern calls. The compiler requires collection to implement java.lang.Iterable, calls its iterator() method once to obtain an Iterator, and then translates the loop body into a while loop that calls hasNext() before each iteration and next() to obtain the current element and advance. This is why every standard Java collection — ArrayList, HashSet, TreeMap's keySet(), and so on — supports identical for-each syntax despite having completely different internal storage: they all implement the same Iterable contract, so the compiler can treat them uniformly regardless of what's actually happening underneath.
Facade Pattern
What's the difference between what the Facade pattern and the Adapter pattern are each trying to solve?
intermediateAdapter solves an incompatibility problem: two interfaces don't match, and the adapter translates one shape into the other so a client can use a class it otherwise couldn't. Facade solves a complexity problem: the underlying subsystem's interface is already perfectly usable, it's just made up of several classes and calls that need to happen in a specific order to accomplish one task. A facade doesn't translate anything -- it coordinates. Mixing the two up in practice usually shows up as reaching for an adapter to hide multi-step complexity, which doesn't actually simplify anything since the adapter still has to expose every step, or reaching for a facade to fix a genuine interface mismatch, which doesn't compile because the facade's method signature still doesn't match what the client needs.
How can a Facade class turn into a god object over time, and what's the usual way to prevent that?
intermediateA facade starts out coordinating a handful of related calls behind one clean method, which is exactly its intended job. The risk shows up as the application grows: every new client task that happens to need several subsystems gets added as one more method on the same facade class, because it's already sitting there and already has references to everything. Left unchecked, that facade ends up responsible for coordinating dozens of unrelated tasks across the entire system, becoming a single class that's hard to change safely because almost every feature touches it somehow. The usual fix is splitting one overloaded facade into several smaller, more focused facades -- one per genuine use case or per type of client -- so each one keeps a narrow, coherent responsibility instead of accumulating everything by default.
How does a typical Spring Boot @Service class often act as a Facade, even without anyone naming it that way?
intermediateA service class in a layered Spring application commonly sits between a controller and several lower-level collaborators -- a repository for persistence, a validator for business rules, maybe an event publisher for notifying other parts of the system. The controller calls one method, like placeOrder(), and that single method internally coordinates all of those collaborators in the correct sequence, then returns one combined result. The controller never constructs the repository or the publisher itself and never needs to know how many steps placing an order actually involves. That's precisely the shape of a Facade: one simple, high-level entry point hiding a multi-step coordination sequence across several classes, even though the class is labeled @Service rather than explicitly called a facade.
Mediator Pattern
What problem does introducing a mediator object actually solve in a system where many objects need to communicate?
intermediateIt replaces a web of direct references between every pair of communicating objects with each object holding a single reference to one central coordinator. Without a mediator, N objects that all need to talk to each other require roughly N-squared direct connections, and adding one more participant can mean updating many existing objects to know about it. With a mediator, every object needs exactly one reference — to the mediator — and the mediator is the only object that ever needs to know the full set of participants. This turns an unmanageable tangle of pairwise dependencies into a simple hub-and-spoke structure that is far easier to reason about and extend.
Does the Mediator pattern eliminate coupling between objects, or does it just move it somewhere else?
intermediateIt concentrates coupling rather than eliminating it. All the complexity that used to be spread thin across many objects' direct references to each other now sits inside one mediator class, which has to know about, and coordinate, every participant. This is a genuinely worthwhile trade in most cases — many small, tangled dependencies become one well-understood, centralized one — but it is not free: a mediator that keeps absorbing more responsibilities, such as routing, logging, and moderation, without discipline can grow into its own unwieldy god object. The fix when that happens is not to abandon the mediator, but to split those responsibilities into smaller collaborating classes that the mediator coordinates rather than implements directly.
In a chat-room mediator, why does the mediator need to distinguish between a user sending and other users receiving a message?
intermediateThe mediator's broadcast method is called once by the sender, but it needs to deliver the message to every other participant without echoing it back to the sender itself. If the mediator called the exact same method on every participant, including the sender, the sender would receive its own message back — a subtle bug that is easy to miss without deliberately testing for it. By checking that a given participant is not the original sender before delivering to them, and by exposing a distinct receive method that the mediator calls, as opposed to the send method a participant calls to initiate a broadcast, the mediator makes the sender-and-receiver distinction explicit rather than accidental.
State Pattern
How does the State pattern differ from simply using an enum with a switch statement to control behavior?
intermediateThe State pattern extracts each behavioral case into its own class implementing a shared interface, while an enum-and-switch keeps all cases as branches inside every method that needs to vary. The practical difference shows up when a new case is added: with State, you write one new class and touch nothing else; with an enum and switch, you have to find and correctly update every method that branches on that enum, and it is easy to update most of them and silently miss one. State also lets each case hold its own behavior — and even its own transition logic — in one place, whereas a switch scatters the logic for one case across however many methods branch on it. In short, State trades a small amount of extra class-file overhead for a much stronger guarantee that adding new behavior cannot accidentally break existing code paths.
What is a self-transitioning state, and how is it different from the simpler, caller-driven use of the State pattern?
intermediateIn the caller-driven form, external code explicitly decides which state is active, for example by calling a setMode() method with the state it wants. In a self-transitioning state machine, the current state object decides on its own, as part of handling an event, what the next state should be — for instance, a media player's StoppedState can call player.setState(new PlayingState()) itself when pressPlay() is handled, so the caller never picks PlayingState directly. This self-transitioning form is closer to how state machines are usually described in theory, since the rules for what comes next live inside the states rather than in the caller. Both are legitimately the State pattern; which one is appropriate depends on whether the 'what happens next' logic genuinely belongs to the object's own internal rules or to an external decision-maker.
When is it safe to share a single instance of a state class across multiple context objects, and when does that become a bug?
intermediateIt is safe when the state class is stateless — it holds no fields specific to any one context, so calling its methods on behalf of different objects can never leak data between them. A Walking state object that just returns a fixed ETA and a fixed directions string is a good example: every DirectionService in an application could share the exact same Walking instance without any risk. It stops being safe the moment a state needs to track something specific to the particular context it is attached to, such as how long that context has been in that state. At that point, two different contexts sharing one state instance would silently read and overwrite each other's data through the same shared object, which is a genuinely hard bug to trace back to its cause.
Decorator Pattern
Why does the Decorator pattern scale better than subclassing when you have several optional, independently combinable features?
intermediateSubclassing models each combination of features as its own class, so the number of classes needed grows roughly as 2^n for n independent optional features -- with even a handful of features, that becomes impractical to write and test. Decorator instead models each feature as its own small wrapper class that implements the same interface as the thing it wraps, so any subset of features can be composed at runtime by wrapping the base object in whichever decorators are needed, in whatever order. This turns a combinatorial class explosion into a linear number of decorator classes, and it lets combinations be assembled dynamically rather than requiring every combination to be anticipated and hand-coded in advance.
Decorator and Proxy have nearly identical class structures. How do you tell which pattern you're actually looking at?
intermediateBoth patterns wrap an object behind the same interface it implements, so structurally a decorator and a proxy can look almost indistinguishable in a class diagram. The real difference is intent. A decorator's job is adding new behavior that composes freely with other decorators -- it's common to stack several of them together, like several toppings on a pizza or several stream wrappers around a file. A proxy's job is controlling access to the real object -- lazy loading, caching, permission checks, or logging -- and proxies are typically used singly rather than stacked. A useful question to ask when reading unfamiliar code: is this wrapper adding a genuinely new capability, or deciding whether, when, and how the real object gets used?
Can the order in which decorators are stacked change the final behavior of the wrapped object, and if so, how?
intermediateYes, whenever a decorator's effect isn't simply additive. If every decorator just adds a fixed amount to some value, like a flat topping cost, stacking order doesn't change the final total. But if one decorator applies a percentage change -- a discount or a tax -- while another applies a flat amount, the final result depends on which one runs first, since a percentage calculated before versus after a flat addition produces different numbers. This is worth checking deliberately whenever decorators are combined: additive, order-independent decorators can be stacked freely, but decorators with multiplicative or conditional effects need their stacking order treated as a real design decision.
Dependency Inversion Principle
What's the difference between Dependency Inversion and Dependency Injection?
intermediateDependency Inversion is a design principle: high-level modules and low-level modules should both depend on a shared abstraction, rather than the high-level module depending directly on the low-level module's concrete details. Dependency Injection is a technique for supplying an object's dependencies from outside — typically through a constructor, setter, or framework — instead of the object constructing them internally itself. The two are related but not interchangeable: you can inject a dependency through a constructor and still violate Dependency Inversion, if the constructor parameter is typed as a concrete class rather than an interface, since the coupling has simply moved rather than actually being removed. Genuine Dependency Inversion requires both pieces together — an abstraction for both sides to depend on, and some mechanism, usually injection, for supplying the concrete implementation from outside.
Why does depending on concrete classes instead of interfaces make unit testing harder?
intermediateWhen a class constructs its own concrete dependencies internally, such as a NotificationService creating a real EmailService inside its own constructor, there is no seam where a test can substitute a fake, controllable version of that dependency. That forces any test of the high-level class to also exercise the real low-level implementation, which might mean an actual network call to an email provider, a real database write, or any other slow, unreliable, or side-effect-producing operation just to verify unrelated business logic. When a class instead depends on an interface supplied from outside, a test can pass in a trivial fake implementation that simply records what it was called with, letting the test verify the high-level class's own logic in complete isolation. This is precisely why mocking frameworks like Mockito can only substitute fake implementations for dependencies that were already typed as interfaces or abstract classes in the production code — they cannot meaningfully mock a dependency the class instantiates and holds as a hardcoded concrete type.
How does the Dependency Inversion Principle relate to how Spring's @Autowired dependency injection works?
intermediateSpring's @Autowired mechanism is Dependency Inversion applied automatically at framework scale: a class declares a constructor parameter typed as an interface, and Spring's container is responsible for supplying a concrete bean that implements it at runtime, rather than the class constructing that dependency itself. This mirrors exactly the NotificationService example, where the class's constructor takes a NotificationChannel interface instead of building an EmailService directly — the only difference is that Spring automates the job of deciding which concrete implementation to hand over, instead of that decision being made explicitly in application code. Because the class only ever depends on the interface, Spring can swap in a completely different implementation for different environments or profiles — a real EmailService in production and a fake or test double in a test context — without changing the class's own source at all. This is also exactly why Spring-managed classes are so straightforward to unit test: since dependencies always arrive as interfaces through the constructor, a test can construct the class directly with a hand-written fake, bypassing the Spring container entirely.
Open/Closed Principle
What does it mean for a class to be 'open for extension but closed for modification'?
beginner"Open for extension" means new behavior can be added to a system, and "closed for modification" means adding that new behavior should not require editing a class's existing, already-tested source code. In practice this is usually achieved through polymorphism: a stable class depends on an interface or abstract type, and new behavior is introduced by writing a brand-new class that implements that type, rather than by adding a new conditional branch to the stable class itself. A PaymentProcessor that calls paymentMethod.pay(amount) through an interface is closed, because supporting a new payment method means writing one new class and never touching PaymentProcessor's own source. The benefit is risk isolation — code that already works and is already trusted in production is never put back in play just to add something new.
How would you refactor a large if/else or switch statement that branches on a type string to follow the Open/Closed Principle?
beginnerStart by defining an interface that captures the one behavior every branch actually performs, such as a pay(double amount) method shared across every payment type. Then turn each branch of the if/else chain into its own class implementing that interface, moving the branch's logic directly into that class's method body. Finally, rewrite the original method so it takes the interface type as a parameter and simply delegates to it — for example, paymentMethod.pay(amount) — instead of inspecting a string and branching. After this refactor, adding a new case means writing one new class that implements the interface; the original dispatching method is never edited again, no matter how many new cases are added later.
Is it possible to over-apply the Open/Closed Principle, and what does that look like?
beginnerYes — designing every class to be maximally extensible from the very first line of code is a real and common mistake, sometimes called speculative generality. It shows up as interfaces created for a single implementation that will likely never gain a second one, abstraction layers built around extension points nobody has actually asked for, and constructors accepting strategy objects that are always passed the exact same value in practice. The cost is real: extra indirection makes the code harder to read and navigate for no actual flexibility gained, since the theoretical future case never materializes. OCP earns its value specifically at points in the code that demonstrably change often — a known, recurring source of new cases, like payment methods in a growing fintech product — not as a blanket policy applied everywhere in advance.
Adapter Pattern
When should you actually reach for the Adapter pattern instead of just modifying one of the two mismatched classes directly?
intermediateAdapter earns its place specifically when you don't control one side of the mismatch -- most commonly a third-party library or a legacy system you're not allowed to change. In those cases there's no option to rename a method or reorder its parameters, so a small translating class in between is the only way to make the incompatible class usable through the interface your application already depends on. If you own both classes involved in the mismatch, reaching for an adapter usually just adds an unnecessary layer of indirection; it's simpler to fix the signature mismatch directly. Adapter is a workaround for something outside your control, not a general-purpose way to connect any two interfaces.
How is the Adapter pattern different from the Facade pattern, given that both wrap other classes?
intermediateAdapter and Facade both introduce a class that sits in front of existing code, but they solve different problems. Adapter exists to make one incompatible interface compatible with what a client already expects -- it typically wraps a single class and translates its exact shape, one to one. Facade exists to simplify access to a subsystem that is already functionally correct but complicated to use directly -- it typically coordinates several classes behind one simpler method, without trying to match any specific interface the client already depends on. In short, Adapter is about compatibility between two shapes, while Facade is about hiding coordination complexity behind a simpler entry point.
In Java, why is object composition typically used to build an adapter instead of inheriting from the class being adapted?
intermediateAn adapter built with composition holds a reference to the incompatible class as a field and delegates to it inside its own methods, which works even when the class being wrapped is final, has no default constructor, or is supplied to the adapter at runtime rather than known in advance. Java also only allows single inheritance, so if an adapter needed to extend the adapted class, it could never also extend anything else, and it would still need to implement the target interface separately. Composition keeps the adapter flexible -- it can wrap any instance handed to it, including ones created elsewhere -- and keeps the adapted class's internals genuinely private, exposed only through whatever the adapter's own methods choose to forward.
Memento Pattern
In the Memento pattern, why shouldn't the Caretaker be able to read the contents of a Memento it's holding?
intermediateThe Caretaker's entire job is to manage when snapshots get taken and restored, not to know anything about what's inside them — that knowledge belongs solely to the Originator that created the snapshot. If the Caretaker could freely read a Memento's fields, the Originator's internal representation would leak out to a class that has no business understanding it, defeating the point of encapsulation. In Java this is often enforced by giving the Memento's data-access method package-private (default) visibility instead of public, so only classes in the same package — realistically just the Originator — can call it. Letting the Caretaker peek inside also creates a hidden coupling: if the Originator's internal fields ever change shape, any code that was reading them directly would break, even though that code was never supposed to depend on those details in the first place.
How would you extend a Memento-based undo implementation to also support redo?
intermediateAdd a second stack, typically called something like redoHistory, alongside the existing undo history stack. Whenever undo() runs, before restoring the popped memento, push the state the object is currently in — the one being moved away from — onto the redo stack. A subsequent redo() call then pops from that redo stack and restores from it, moving the object forward again. One detail matters: any new saveState() call after a fresh edit should clear the redo stack, since redoing into a state that's no longer consistent with the edit history would be incorrect — this mirrors how undo/redo works in every real text editor, where making a new edit after an undo discards the redo trail.
What's a genuine practical cost of using the Memento pattern that you should be ready to discuss in an interview?
intermediateMemory usage is the honest answer. Every saved memento holds a full snapshot of the originator's state, and if that state is large or snapshots are taken frequently — say, on every keystroke in an editor — the history can grow large very quickly and stay alive for as long as the Caretaker holds it. Real systems usually manage this deliberately: capping history to a fixed number of entries and evicting the oldest one once the limit is hit, or only snapshotting at meaningful checkpoints rather than on every single change. There's a second, related cost worth mentioning: if any of the originator's fields are mutable objects rather than immutable values, saving them correctly requires a deep copy rather than just storing a reference, which adds both memory and CPU overhead on every save.
Single Responsibility Principle
How do you identify whether a class violates the Single Responsibility Principle?
beginnerTry to describe the class's job in a single sentence without using the word "and" — if you genuinely can't, each "and" usually marks a separate responsibility that should be pulled into its own class. A more structural signal is asking what would force this class to change: if you can list multiple, unrelated triggers — a new database, a new email provider, a new tax rule — and none of them depend on each other, the class is bundling independent axes of change. Watch also for classes that mix a noun (like Invoice, representing data) with verbs that belong to unrelated concerns (like saveToDatabase or sendEmail) — that mixing is one of the most common real-world SRP violations. The fix is not to count methods, since a class can have many methods and still have one responsibility, or have very few methods and still violate SRP if those few methods serve unrelated purposes.
Does the Single Responsibility Principle mean a class should only ever have one method?
beginnerNo — that's a common misreading. SRP is about a class having one reason to change, not a fixed method count. A class can legitimately have many methods as long as all of them serve the same underlying responsibility; for example, an Invoice class with generateInvoice(), getAmount(), and getFormattedTotal() can still satisfy SRP because every one of those methods only changes when billing or display rules change. Conversely, a class with only two methods can violate SRP if those two methods represent unrelated concerns, such as one line of billing logic and one line of email-sending logic packed into a tiny class. Over-applying the "one method per class" misreading is itself a design mistake, since it creates unnecessary indirection without tracking any genuine, independent axis of change.
How does the Single Responsibility Principle relate to the layered architecture used in a typical Spring Boot application?
beginnerA typical Spring Boot application's @Entity, @Repository, and @Service layers are SRP applied at the architectural level: the entity holds and describes data, the repository owns persistence, and the service owns business logic, and each layer changes for a completely different reason. This mirrors splitting an Invoice class into Invoice, InvoiceRepository, and EmailService — a database migration only touches the repository layer, a business rule change only touches the service layer, and a field rename only touches the entity, without any of those changes rippling into the other layers. This separation is also what makes each layer independently testable — a service can be unit tested with a mocked repository, without spinning up a real database, precisely because persistence logic was never mixed into the service in the first place. In short, SRP isn't just a rule for individual classes; it's the same reasoning, applied consistently, that produces a well-layered backend architecture.
OOP Recap & UML
What is the difference between compile-time polymorphism (method overloading) and runtime polymorphism (method overriding) in Java?
beginnerCompile-time polymorphism happens through method overloading — multiple methods with the same name but different parameter lists in the same class — and the compiler decides which one to call based on the argument types at compile time. Runtime polymorphism happens through method overriding, where a subclass provides its own implementation of a method already defined in its parent, and which version actually runs is decided at runtime based on the real object behind the reference, not its declared type. A classic example is a `PaymentMethod` reference that could point to a `CreditCard` or a `UPI` object at runtime — the same line of code, `method.pay(amount)`, calls a different implementation depending on what `method` actually references when the line executes. Overloading is resolved statically by the compiler using the method signature; overriding is resolved dynamically by the JVM using the object's actual class, which is why it's also called dynamic dispatch.
When designing two related classes, how do you decide whether to use inheritance or composition?
beginnerUse inheritance when the relationship is a genuine, unconditional "is-a" and the subclass needs to reuse the parent's actual implementation and state — for example, a CreditCard genuinely is a Card and shares its fields. Use composition when one class merely needs to use another's capability without being a specialized version of it — for example, a PaymentService is not a kind of PaymentMethod, it simply holds and calls one. A useful test is to ask whether every instance of the subclass could honestly answer "yes" to every promise the parent makes; if a subclass has to override a method to throw an exception or leave it empty, that's usually a sign composition would have been the better fit. In general, composition is the safer default because it produces looser coupling — the containing class depends only on the other object's public interface, not its implementation details — and that's why "favor composition over inheritance" is repeated so often in object-oriented design.
What is the difference between an abstract class and an interface in Java, and when would you choose one over the other?
beginnerAn abstract class can hold shared state (fields) and partially implemented behavior, and is meant for classes that are genuinely related and share a common identity, like Card being the shared parent of CreditCard and DebitCard. An interface holds no state at all — only method signatures every implementer must fulfill — and is meant for a capability that unrelated classes can all satisfy, like PaymentMethod being implemented by both a Card subclass and an unrelated UPI class. A class can extend only one abstract class but can implement any number of interfaces, so interfaces are also the right tool when a class needs to satisfy multiple independent contracts at once. As a rule of thumb: reach for an abstract class when subclasses share real data and some default logic, and reach for an interface when the only thing shared is a promise about behavior.
Prototype Pattern
What is the difference between a shallow copy and a deep copy, and why does it matter when writing a clone() method?
intermediateA shallow copy duplicates an object's top-level fields, but if a field is a reference to a mutable object, both the original and the copy end up pointing at that same shared object. A deep copy goes further and recursively clones any mutable referenced object too, so the copy is fully independent. This matters because a shallow copy creates a hidden coupling: mutating the 'copy' can silently mutate the original, since they're really sharing state underneath. When writing a clone() method, every mutable reference field needs to be explicitly cloned rather than just reassigned, while genuinely immutable fields like Strings or primitives are safe to copy by value directly.
Why might a team choose a hand-written Prototype interface over Java's built-in Cloneable and Object.clone()?
intermediateObject.clone() performs a shallow, field-by-field copy by default, and Cloneable itself is just a marker interface with no methods to enforce — it's easy to implement it incorrectly or forget to override clone() for a class that has mutable fields. A hand-written Prototype<T> interface with an explicit clone() method forces every implementing class to decide, field by field, whether each piece of state needs a deep copy or can be safely shared or copied by value. This makes the deep-versus-shallow decision visible in code review and easy to reason about, instead of relying on inherited default behavior that can silently produce shared mutable state between an object and its clone.
When does cloning an existing object actually give a real performance advantage over just constructing a new one?
intermediateCloning pays off when constructing an object from scratch is genuinely expensive — for example, if construction involves parsing a large configuration, running an expensive computation, or populating many fields from a database or file. If a fully-configured prototype already exists in memory, copying it is typically far cheaper than repeating that setup work for every new instance, which is why patterns like spawning many similar game entities from one prototype are common. If constructing an object is already cheap — just assigning a few primitive fields — cloning offers little to no advantage and mainly adds the complexity of deciding what to deep-copy, so it's worth reserving Prototype for cases where construction cost is the actual bottleneck.
Observer Pattern
Why does hardwiring a Subject class directly to a concrete Observer type create an Open/Closed Principle violation?
intermediateOnce a Subject holds a direct reference to one specific listener type — a field of that exact class, set through the constructor — every new listener type that comes along forces the Subject's own source code open again: a new field, a new line inside whatever method pushes out notifications. That means code which was already written, tested, and shipped has to be modified every time the roster of interested parties grows, which is exactly what the Open/Closed Principle says should not happen. The fix is to have the Subject depend only on a shared Observer interface and hold a collection of that interface type, so new listeners plug in by implementing the interface and calling attach(), with zero changes to the Subject's existing, already-tested code.
How can the Observer pattern cause a memory leak, and how would you prevent it?
intermediateA Subject's observer list holds a strong reference to every attached observer for as long as the Subject itself is alive. If an observer — say, a UI component or a session-scoped object — gets logically discarded elsewhere in the application but is never explicitly detached, the Subject keeps it, and everything it in turn references, reachable in memory indefinitely, even though nothing else in the program still needs it. This is a genuinely common real bug, especially in long-lived subjects like application-wide event buses. The fix is discipline plus tooling: always pair every attach() with a corresponding, reliably-called detach(), often tied to a component's own lifecycle hooks (like a UI framework's onDestroy), or use weak references for the observer list so an observer can be garbage collected even if something forgot to detach it explicitly.
Java's standard library included java.util.Observer and java.util.Observable since version 1.0 — why were they deprecated?
intermediateBoth types were deprecated in Java 9 because their design had real, structural limitations that a hand-rolled implementation of the pattern avoids. Observable is a class, not an interface, so any class that wanted to be observable had to extend it — and since Java only allows single inheritance, that used up the one inheritance slot a class gets, blocking it from extending anything else. The API also offered no guarantees around notification order or thread-safety, and its setChanged()/notifyObservers() split made it easy to forget to mark state as changed before notifying. Modern Java code either hand-rolls the pattern with a custom interface, exactly as shown when building it from scratch, or uses the reactive java.util.concurrent.Flow API introduced in Java 9, which was designed with backpressure and proper interface-based composition in mind.
Composite Pattern
What signal tells you a design problem is a genuine fit for the Composite pattern?
intermediateComposite fits when you have a real part-whole tree structure -- individual items and groups of items that need to nest arbitrarily deep -- and you want client code to operate on a single item and an entire subtree through identical method calls. File systems, UI component trees, and organizational charts are classic examples, because in each case a leaf and a branch both respond meaningfully to the same operation, like computing a total size or rendering a layout. If the domain data doesn't actually nest, or if leaf and container objects need fundamentally different operations with little in common, forcing a shared component interface just adds indirection without buying anything -- Composite specifically pays off when uniform treatment of nodes and subtrees is the actual requirement.
What bug can occur in a Composite tree structure if you don't guard against it, and how would you prevent it?
intermediateA basic Composite implementation has no built-in protection against a composite node containing itself, either directly or through a longer chain of nested composites. If that happens, any recursive operation over the tree -- computing a total size, printing details, walking the structure -- recurses forever and eventually crashes with a stack overflow. Preventing this means adding an explicit check before an add operation completes: walking up from the node being added to confirm the container being added to isn't already a descendant of it, and rejecting the operation if it is. Real file systems and most UI frameworks prevent this structurally as part of how their trees are built, but a from-scratch Composite implementation needs to add that guard deliberately if cycles are even remotely possible.
How should you handle an operation, like adding a child, that only makes sense on composite nodes and not on leaves?
intermediateThere are two common approaches, and both are legitimate trade-offs rather than one being universally correct. The first keeps the operation off the shared component interface entirely and defines it only on the concrete composite class, meaning leaf objects simply don't have the method at all -- this respects interface segregation, since no class is forced to expose an operation it can't meaningfully support, but it means client code needs to know it's holding a composite specifically before it can call that method. The second puts the operation on the shared interface for uniformity, and has leaf classes implement it by throwing an exception like UnsupportedOperationException -- every node looks identical from the outside, but callers can now hit a runtime error if they call it on the wrong kind of node. Which one to choose depends on whether uniform typing or compile-time safety matters more for the specific codebase.
Builder Pattern
Why does a single constructor with many parameters, some of them optional, become a real risk rather than just an inconvenience?
intermediateOnce a constructor has several parameters of the same type, several booleans in a row, for example, nothing stops a caller from passing them in the wrong order, and the mistake compiles perfectly fine while silently constructing the wrong object. There is also no clean way to support 'give me an object using only the mandatory fields, defaulted otherwise' without either a second constructor whose parameter types happen to differ, or writing a constructor for every meaningful combination of optional fields present or absent, which can approach two-to-the-power-of-n constructors for n optional parameters. Builder fixes both problems: each field is set by a clearly named method rather than by position, so a swapped argument cannot happen, and any optional field can simply be skipped, with a sensible default applied automatically.
Why is a builder for a class commonly implemented as a static nested class rather than a separate top-level class?
intermediateStatic, because building an object does not require an existing instance of the class being built to already exist — you are using the builder specifically to create that first instance. Nested, specifically so the builder can access the outer class's private members, including a private constructor: making the target class's real constructor private is what actually forces every caller to go through the builder, since no code outside the class could otherwise call new on it directly. A top-level builder class would have no special access to a private constructor in a different class, so nesting is what makes the you-can-only-construct-this-through-the-builder guarantee possible rather than just conventional.
How does the Builder pattern differ from the Factory pattern, given that both are considered creational patterns?
intermediateThey solve genuinely different problems despite both being about object creation. Factory is about hiding which concrete class gets instantiated behind one method, so callers do not need to know or name the specific class themselves. Builder is about making the construction of one specific, often complex object readable and safe when it has many parameters, especially optional ones, regardless of whether more than one concrete class is involved at all. The two are not mutually exclusive — a factory method can internally use a builder to actually assemble the object it eventually returns, combining which class to create with how to safely construct it in a single call.
Bridge, Visitor & Chain of Responsibility
How do you tell the Bridge pattern apart from the Strategy pattern when both involve a class holding a reference to an interface it delegates to?
advancedStructurally, the two can look identical -- a class holds a field typed as an interface, and calls methods on it instead of implementing that behavior itself. The distinction is about intent and about how many hierarchies are actually varying, not about the shape of the code. Strategy is about swapping one algorithm for a single class -- there is one hierarchy of concrete types, and the interface field is the only axis of variation. Bridge is about letting two entire class hierarchies, each with their own family of subclasses, vary independently of each other, connected only through that one composition link. If you find two independent dimensions of variation that both need their own subclasses -- like remote types and device types -- that is the signal you are looking at Bridge rather than Strategy.
How does the Bridge pattern prevent the class explosion that happens when you model every combination of two independent hierarchies as its own class?
advancedModeling every combination directly, such as one class per remote-type-and-device-type pairing, needs N times M classes for N remote types and M device types -- 50 classes for just 5 remotes and 10 devices, with logic duplicated across every class sharing a device or a remote type. Bridge fixes this by recognizing that the two dimensions are genuinely independent and should never have been combined into one hierarchy at all. It splits them into two separate hierarchies -- an abstraction hierarchy and an implementation hierarchy -- and connects them through one composition reference held by the abstraction. That turns the class count from N times M into N plus M, which is 15 for the same 5-and-10 example, and the gap only widens as either dimension keeps growing.
What does double dispatch mean in the Visitor pattern, and how does it let a program run different logic per concrete type without any instanceof checks?
advancedDouble dispatch is two separate method-resolution steps happening back to back. The first dispatch is ordinary runtime polymorphism: calling accept() on an object typed as the general interface resolves, at runtime, to that object's real concrete type's implementation -- for example, PDFDocument's accept(), not some generic version. The second dispatch happens inside that accept() method, where the call is visitor.visit(this): because the compiler statically knows, at that exact line, that this is a PDFDocument, it selects the visit(PDFDocument) overload specifically, not any of the other overloads on the visitor interface. Together, those two dispatches let a visitor run type-specific logic for every concrete type it encounters, entirely through the type system, without a single instanceof check or manual type inspection anywhere in the code.
What trade-off does the Visitor pattern make, and when does that trade-off actually work in your favor?
advancedVisitor makes adding a new operation trivial: a brand-new class implementing the visitor interface, with zero changes required to any existing element type. But it makes adding a new element type expensive: every existing visitor implementation must add a new overload to handle it, or the code fails to compile. This is the inverse of the trade-off ordinary polymorphism usually makes, where new types are cheap to add and new operations mean touching every existing type. Visitor is the right choice specifically when the set of concrete types is stable and unlikely to grow, while new operations on those types are expected to keep arriving over time -- exactly the situation with a fixed set of document formats that need an ever-growing list of operations performed on them.
In the Chain of Responsibility pattern, does the object sending a request need to know in advance which handler will actually process it?
advancedNo, and that is the entire point of the pattern. The sender only ever holds a reference to the first handler in the chain and calls its handle method. Each handler in turn decides, purely for itself, whether it can process the request; if it cannot, it passes the request to the next handler it holds a reference to, without needing to know anything about who comes after that. The request effectively travels along the chain until some handler capable of processing it does, and which handler that turns out to be emerges from the chain's structure and each handler's own logic at request time, not from any decision made by the original sender.
Why does the order in which handlers are linked together in a Chain of Responsibility matter more than layering order typically does when stacking purely additive behavior?
advancedWhen behavior is purely additive -- each layer only adds something on top of what came before, without changing whether processing continues -- the final combined effect is often the same regardless of stacking order. Chain of Responsibility is different because each handler makes an actual routing decision: it decides whether to handle a request outright or pass it along, and that decision can depend on the request's specific properties. Putting a specialized handler earlier in the chain means it sees requests first and may intercept ones a later, more general handler would have processed differently, or vice versa. Because the outcome for a given request can genuinely change based on which handler encounters it first, assembling the chain in the right order is a real design decision that deserves as much attention as writing each handler's internal logic.
Capstone: Ride-Sharing App
In a ride-sharing service where fare calculation branches on a raw vehicle type string, which principle is being violated and which pattern fixes it?
advancedBranching on a raw type string inside a shared calculation method violates the open-closed idea that existing, tested code should not need to be reopened every time a new variant is added -- supporting a new vehicle type means editing that same if/else chain directly. It is closely related to a Liskov substitution problem too, since there is no real type hierarchy in play at all: every vehicle is the same concrete class wearing a different label, rather than a genuinely substitutable subtype. The fix is to replace the string with a real abstract method on an abstract Vehicle class -- each concrete vehicle type answers for its own fare-per-kilometer polymorphically, and adding a new vehicle type becomes a small new class with no existing code touched at all.
In a ride-sharing system's design, why should fare strategy (standard, shared, luxury) and vehicle type (car, bike) be modeled as two separate hierarchies instead of one combined hierarchy?
advancedThey vary along genuinely independent axes: which vehicle got assigned to a ride has nothing to do with which pricing scheme the passenger selected, and any vehicle can legitimately be combined with any pricing scheme -- a bike under a luxury strategy, a car under a shared strategy, both are meaningful. Combining them into one hierarchy, such as a LuxuryCarFare and a SharedBikeFare class for every combination, would need a class per pairing, growing multiplicatively as either vehicle types or pricing schemes are added. Keeping Vehicle and FareStrategy as two separate hierarchies, connected only where a Ride computes its fare by calling the strategy with the vehicle as an argument, means either dimension can grow independently without touching the other, and any combination of the two just works automatically.
How can a ride-sharing app's ride-status notifications be an example of the Observer pattern even if the code has no class explicitly named Observer or Subject?
advancedThe Observer pattern's defining idea is that one object's state change automatically informs multiple dependent objects, without those dependents needing to poll for the change themselves. In a ride's status-update method, changing status and then calling notify on both the passenger and the driver through a shared inherited method matches that idea exactly -- one state change, several dependents informed automatically, in the same method call. A formal Observer interface with attach and detach methods for dynamically managing a list of subscribers is a common, more general implementation of that idea, but it is not required for the pattern to be genuinely present. What matters for recognizing the pattern is the underlying relationship between the subject and its dependents, not whether the class names or interface names match a textbook template.
Why is a dedicated matching service that coordinates drivers and passengers described as a Mediator, and what would be lost if Driver and Passenger referenced each other directly instead?
advancedA Mediator is a dedicated coordinating object that sits between two or more kinds of collaborators so that none of them need direct references to each other -- all communication and coordination routes through the mediator instead. A ride-matching service that is the only class holding lists of drivers and exposing the operation to request a ride fits this exactly: Driver and Passenger objects never hold references to each other at all. If they referenced each other directly instead, every change to how matching, notification, or ride lifecycle worked would risk touching both classes, and the coupling between every driver and every passenger they might ever interact with would grow unmanageable as the system scaled. Centralizing that coordination in one class keeps Driver and Passenger focused purely on representing a user, while all the matching logic lives in exactly one place.