Coding Interview: OOP Design Patterns — Strategy, Observer, Factory, Singleton, Decorator, Adapter, SOLID

Object-oriented design questions test your ability to model real-world systems using classes, interfaces, and design patterns. These questions appear frequently in interviews at Amazon, Google, and Microsoft, especially for senior roles. This guide covers the design patterns most commonly tested, the SOLID principles that guide good design, and how to approach OOP design questions systematically.

SOLID Principles

SOLID is the foundation of good object-oriented design. (1) Single Responsibility Principle (SRP) — a class should have one reason to change. A UserService that handles authentication, profile updates, and email notifications violates SRP. Split into AuthService, ProfileService, and NotificationService. (2) Open/Closed Principle (OCP) — classes should be open for extension but closed for modification. Add new behavior by creating new classes (subclasses, strategy implementations), not by modifying existing code. (3) Liskov Substitution Principle (LSP) — subtypes must be substitutable for their base types. If a function accepts a Shape, it should work correctly with any Shape subclass (Circle, Rectangle). A Square that inherits from Rectangle but changes both width and height when one is set violates LSP. (4) Interface Segregation Principle (ISP) — clients should not be forced to depend on interfaces they do not use. Split a large IWorker interface (work, eat, sleep) into IWorkable and IFeedable — a robot implements IWorkable but not IFeedable. (5) Dependency Inversion Principle (DIP) — depend on abstractions, not concretions. A PaymentProcessor should depend on a PaymentGateway interface, not on a concrete StripeGateway class. This allows swapping Stripe for PayPal without modifying PaymentProcessor.

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Use when: you have multiple ways to perform an operation and want to select the algorithm at runtime. Example: a navigation app that supports different routing strategies — shortest distance, fastest time, avoid highways. Define a RouteStrategy interface with a method calculateRoute(origin, destination). Implement ShortestDistanceStrategy, FastestTimeStrategy, and AvoidHighwaysStrategy. The Navigator class holds a reference to a RouteStrategy and delegates route calculation to it. The user selects a strategy, and the Navigator uses it without knowing the implementation details. Interview application: “Design a payment system that supports multiple payment methods.” Define a PaymentStrategy interface with process(amount). Implement CreditCardPayment, PayPalPayment, CryptoPayment. The Checkout class uses whichever strategy the user selects. Adding a new payment method requires only a new strategy class — no modification to Checkout (Open/Closed Principle).

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects: when one object (the subject) changes state, all its dependents (observers) are notified automatically. Use when: multiple components need to react to state changes without tight coupling. Example: a stock price ticker. The StockMarket (subject) maintains a list of observers. When a stock price changes, it calls notify() which iterates through all observers and calls their update(stock, price) method. Observers include: a PriceAlertService (sends alerts when price crosses a threshold), a PortfolioTracker (updates portfolio value), and a ChartRenderer (updates the price chart). Adding a new observer requires no changes to StockMarket — just implement the Observer interface and register. Interview application: “Design a notification system for an e-commerce platform.” Events (OrderPlaced, PaymentFailed, ItemShipped) are subjects. Observers include: EmailNotifier, SMSNotifier, PushNotifier, AnalyticsTracker. Each observer decides independently whether and how to handle each event. The event system is decoupled from the notification channels.

Factory and Abstract Factory Patterns

The Factory Method pattern defines an interface for creating objects but lets subclasses decide which class to instantiate. Use when: the exact type of object to create depends on runtime conditions. Simple Factory: a static method that returns different types based on input. NotificationFactory.create(“email”) returns an EmailNotification; create(“sms”) returns SMSNotification. This centralizes object creation and hides implementation details. Factory Method: an abstract class defines a factory method that subclasses override. A DocumentCreator has an abstract createDocument() method. WordDocumentCreator returns a WordDocument; PDFDocumentCreator returns a PDFDocument. Each creator knows how to construct its specific document type. Abstract Factory: a factory of factories. A UIFactory creates related families of UI components: WindowsUIFactory creates WindowsButton, WindowsCheckbox, WindowsTextfield. MacUIFactory creates MacButton, MacCheckbox, MacTextfield. The application uses the factory interface and works with any platform without knowing the concrete classes. Interview tip: when you need object creation that varies by type, reach for a factory. It keeps the creation logic in one place and follows the Open/Closed Principle.

Decorator Pattern

The Decorator pattern attaches additional behavior to an object dynamically without altering its interface. Use when: you need to add responsibilities to individual objects, not to an entire class, and you want to combine behaviors flexibly. Classic example: a coffee ordering system. A base Coffee class with cost() and description(). Decorators: MilkDecorator, SugarDecorator, WhipCreamDecorator. Each wraps a Coffee object and adds to its cost and description. new WhipCreamDecorator(new MilkDecorator(new Espresso())) creates an espresso with milk and whip cream. Cost is computed by chaining: whip cream cost + milk cost + espresso cost. This avoids a class explosion (EspressoWithMilk, EspressoWithMilkAndSugar, …). Real-world usage: Java I/O streams (BufferedInputStream wraps FileInputStream wraps InputStream), middleware chains in web frameworks (logging middleware wraps authentication middleware wraps the handler). Interview application: “Design a text formatting system.” Base: PlainText. Decorators: BoldDecorator, ItalicDecorator, UnderlineDecorator. Each wraps the text and adds formatting tags. Decorators can be combined in any order.

Singleton Pattern and Its Problems

The Singleton pattern ensures a class has only one instance and provides a global access point. Use cases: database connection pools, configuration managers, logging services, thread pools. Implementation: a private constructor, a private static instance variable, and a public static getInstance() method. Thread-safe implementation: use double-checked locking or initialize the instance in a static block (eager initialization). Problems with Singleton: (1) Global state — singletons are effectively global variables, making it hard to reason about state and dependencies. (2) Testing difficulty — singletons cannot be easily mocked or replaced in tests. Code that calls DatabaseConnection.getInstance() is tightly coupled to the concrete class. (3) Hidden dependencies — a class that uses a singleton internally has an invisible dependency that is not expressed in its constructor or interface. Better alternative: dependency injection. Instead of a class creating or fetching its dependencies, inject them through the constructor. The DI container (Spring, Guice) manages the lifecycle and ensures single instances where needed. In interviews: know the Singleton pattern and its implementation, but acknowledge its drawbacks and suggest DI as the modern alternative.

Approaching OOP Design Questions in Interviews

Step-by-step framework: (1) Clarify requirements — what are the core use cases? For “design a parking lot”: how many levels, what vehicle types, what operations (park, leave, find available spot)? (2) Identify the key objects — nouns in the requirements become classes. Parking lot, level, parking spot, vehicle, ticket. (3) Define relationships — a ParkingLot has many Levels. A Level has many ParkingSpots. A ParkingSpot can hold one Vehicle. A Ticket links a Vehicle to a ParkingSpot with entry time. (4) Define interfaces and methods — ParkingLot: parkVehicle(vehicle), leaveVehicle(ticket), getAvailableSpots(vehicleType). ParkingSpot: isAvailable(), park(vehicle), removeVehicle(). (5) Apply design patterns where appropriate — Strategy pattern for different parking fee calculations (hourly, daily, monthly). Observer pattern to notify display boards when spot availability changes. Factory pattern to create different vehicle types. (6) Consider extensibility — what if we add electric vehicle charging spots? Subclass ParkingSpot as ElectricParkingSpot with a startCharging() method. The design should accommodate new features without modifying existing classes.

Scroll to Top