Object-Oriented Design Patterns for Coding Interviews
Design pattern questions test whether you can structure code for maintainability and extensibility. Common in system design warm-ups and low-level design (LLD) rounds at Amazon, Uber, LinkedIn, and Atlassian.
Creational Patterns
Singleton
Ensures only one instance of a class exists. Used for config managers, logging, thread pools.
class Singleton:
_instance = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
# Thread-safe version uses double-checked locking in Java/C++
# Python: use module-level instances (modules are singletons by import)
Factory and Abstract Factory
Factory creates objects without specifying exact class. Use when object creation logic is complex or needs to vary.
class PaymentProcessor:
def process(self, amount): raise NotImplementedError
class StripeProcessor(PaymentProcessor):
def process(self, amount): print(f"Stripe: {amount}")
class PayPalProcessor(PaymentProcessor):
def process(self, amount): print(f"PayPal: {amount}")
class PaymentFactory:
@staticmethod
def create(type_):
if type_ == "stripe": return StripeProcessor()
if type_ == "paypal": return PayPalProcessor()
raise ValueError(f"Unknown type: {type_}")
Builder
Constructs complex objects step by step. Useful when object has many optional fields.
class QueryBuilder:
def __init__(self): self._parts = {}
def select(self, cols): self._parts["select"] = cols; return self
def from_table(self, t): self._parts["from"] = t; return self
def where(self, cond): self._parts["where"] = cond; return self
def limit(self, n): self._parts["limit"] = n; return self
def build(self):
q = f"SELECT {self._parts['select']} FROM {self._parts['from']}"
if "where" in self._parts: q += f" WHERE {self._parts['where']}"
if "limit" in self._parts: q += f" LIMIT {self._parts['limit']}"
return q
query = QueryBuilder().select("*").from_table("users").where("age > 18").limit(10).build()
Structural Patterns
Adapter
Converts interface of a class to match what clients expect. Used to integrate incompatible APIs.
# Old interface
class OldLogger:
def write_log(self, msg): print(f"[OLD] {msg}")
# New interface expected by system
class Logger:
def log(self, msg): raise NotImplementedError
class LoggerAdapter(Logger):
def __init__(self, old_logger): self._old = old_logger
def log(self, msg): self._old.write_log(msg)
Decorator
Adds behavior to objects without changing their class. Better than inheritance for optional functionality.
class DataSource:
def write(self, data): raise NotImplementedError
def read(self): raise NotImplementedError
class FileDataSource(DataSource):
def write(self, data): print(f"Write: {data}")
def read(self): return "file_data"
class EncryptionDecorator(DataSource):
def __init__(self, source): self._source = source
def write(self, data): self._source.write(f"encrypted({data})")
def read(self): return f"decrypted({self._source.read()})"
class CompressionDecorator(DataSource):
def __init__(self, source): self._source = source
def write(self, data): self._source.write(f"compressed({data})")
def read(self): return f"decompressed({self._source.read()})"
# Chain decorators
source = CompressionDecorator(EncryptionDecorator(FileDataSource()))
Proxy
Controls access to an object. Use for lazy loading, caching, access control, logging.
class DatabaseProxy:
def __init__(self, real_db, cache):
self._db = real_db
self._cache = cache
def query(self, sql):
if sql in self._cache:
return self._cache[sql]
result = self._db.query(sql)
self._cache[sql] = result
return result
Behavioral Patterns
Observer
Defines one-to-many dependency. When one object changes, all dependents are notified. Used for event systems.
class EventEmitter:
def __init__(self): self._listeners = {}
def on(self, event, fn):
self._listeners.setdefault(event, []).append(fn)
def emit(self, event, *args):
for fn in self._listeners.get(event, []):
fn(*args)
emitter = EventEmitter()
emitter.on("user_created", lambda u: print(f"Send welcome email to {u}"))
emitter.on("user_created", lambda u: print(f"Create profile for {u}"))
emitter.emit("user_created", "alice@example.com")
Strategy
Defines a family of algorithms, encapsulates each, and makes them interchangeable.
class SortStrategy:
def sort(self, data): raise NotImplementedError
class QuickSort(SortStrategy):
def sort(self, data): return sorted(data) # simplified
class MergeSort(SortStrategy):
def sort(self, data): return sorted(data, key=lambda x: x)
class Sorter:
def __init__(self, strategy: SortStrategy): self._strategy = strategy
def sort(self, data): return self._strategy.sort(data)
def set_strategy(self, strategy): self._strategy = strategy
Command
Encapsulates a request as an object. Supports undo, queuing, and logging of operations.
class Command:
def execute(self): raise NotImplementedError
def undo(self): raise NotImplementedError
class TextEditor:
def __init__(self): self.text = ""; self.history = []
def execute(self, command):
command.execute()
self.history.append(command)
def undo(self):
if self.history:
self.history.pop().undo()
class InsertCommand(Command):
def __init__(self, editor, text):
self.editor = editor; self.text = text
def execute(self): self.editor.text += self.text
def undo(self): self.editor.text = self.editor.text[:-len(self.text)]
SOLID Principles Cheatsheet
- S – Single Responsibility: a class should have one reason to change
- O – Open/Closed: open for extension, closed for modification (use inheritance/interfaces)
- L – Liskov Substitution: subclasses must be substitutable for base class
- I – Interface Segregation: many specific interfaces better than one general interface
- D – Dependency Inversion: depend on abstractions, not concretions (inject dependencies)
Interview Tips
- Name the pattern explicitly when using it – interviewers want to see you recognize them
- Singleton is overused – mention thread safety and testability concerns
- Prefer composition over inheritance (Decorator, Strategy over deep class hierarchies)
- Observer enables loose coupling (Publisher/Subscriber model)
- Builder is ideal for config objects with many optional parameters
Companies that ask this: Coinbase Interview Guide
Companies that ask this: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems
Companies that ask this: LinkedIn Interview Guide 2026: Social Graph Engineering, Feed Ranking, and Professional Network Scale
Companies that ask this: Shopify Interview Guide
Companies that ask this: Atlassian Interview Guide
Companies that ask this: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the difference between the Factory and Abstract Factory patterns?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Factory Method creates objects through a method that subclasses can override – the factory is a method. Abstract Factory creates families of related objects through an interface with multiple factory methods – the factory is a class. Use Factory Method when you need one product type with varied implementations. Use Abstract Factory when you need multiple related products that must be compatible (e.g., UI widgets for Windows vs Mac – buttons, checkboxes, and textfields all come from the same factory).”
}
},
{
“@type”: “Question”,
“name”: “When should you use the Decorator pattern over inheritance?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Use Decorator when you need to add behavior to objects at runtime without modifying the class, or when the combination of behaviors would create an explosion of subclasses. Decorator composes behaviors (EncryptedCompressedLogger) while inheritance requires a class for each combination (EncryptedLogger, CompressedLogger, EncryptedCompressedLogger). Java I/O streams are the canonical Decorator example. The Open/Closed principle favors Decorator: extend behavior without modifying existing classes.”
}
},
{
“@type”: “Question”,
“name”: “How does the Observer pattern relate to event-driven systems?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The Observer pattern is the foundation of event-driven architecture. A Subject (Publisher) maintains a list of Observers (Subscribers) and notifies them when state changes. Modern implementations: JavaScript EventEmitter, React useState/useEffect, pub/sub systems (Kafka, Redis Pub/Sub). The key benefit is loose coupling: the Subject does not know anything about Observers beyond the interface. Observers can be added/removed at runtime. The pattern scales from in-process events to distributed message queues.”
}
},
{
“@type”: “Question”,
“name”: “What is the Strategy pattern and how does it enable runtime behavior switching?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Strategy encapsulates a family of algorithms behind a common interface, making them interchangeable. The context delegates the algorithm to the current Strategy object. At runtime, you can swap strategies: a sorting algorithm (QuickSort vs MergeSort), a payment processor (Stripe vs PayPal), or a routing algorithm (Dijkstra vs A*). Strategy follows the Open/Closed principle – add new strategies without modifying the context. Avoids complex if/else or switch statements for algorithm selection.”
}
},
{
“@type”: “Question”,
“name”: “How does the Builder pattern differ from the Factory pattern?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Factory creates objects in one step and hides creation logic. Builder constructs complex objects step-by-step, allowing different configurations. Use Builder when an object has many optional parameters (avoids constructor with 10+ arguments), when construction requires multiple steps, or when you want to reuse the same construction process for different representations. Common examples: SQL query builders, HTTP request builders, test data builders (test fixtures). Java StringBuilder and Python dataclasses approximate the Builder pattern.”
}
}
]
}