0% found this document useful (0 votes)
51 views53 pages

Python LLD Interview Preparation - 4

The SDE-I LLD Playbook provides a detailed guide for mastering Object-Oriented Programming (OOP) in Python, emphasizing the importance of understanding OOP concepts for Low-Level Design interviews. It covers key topics such as classes, instances, the __init__ method, and the four pillars of OOP: encapsulation, inheritance, abstraction, and polymorphism. Additionally, it introduces the SOLID principles of software design, which are essential for creating maintainable and scalable code.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
51 views53 pages

Python LLD Interview Preparation - 4

The SDE-I LLD Playbook provides a detailed guide for mastering Object-Oriented Programming (OOP) in Python, emphasizing the importance of understanding OOP concepts for Low-Level Design interviews. It covers key topics such as classes, instances, the __init__ method, and the four pillars of OOP: encapsulation, inheritance, abstraction, and polymorphism. Additionally, it introduces the SOLID principles of software design, which are essential for creating maintainable and scalable code.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 53

The SDE-I LLD Playbook: A Comprehensive Guide to Acing

the Python Low-Level Design Interview

Part 1: The Bedrock - Mastering Object-Oriented Programming in


Python

A robust understanding of Object-Oriented Programming (OOP) is not merely a


prerequisite for a Low-Level Design (LLD) interview; it is the very language in which
design ideas are expressed and evaluated. For an SDE-I candidate, demonstrating a
deep, nuanced grasp of Python's object model is the first step toward acing the LLD
round. This section moves beyond surface-level definitions to explore the
fundamental building blocks of OOP in Python, focusing on the "why" behind each
concept to build a solid foundation for the advanced design principles and patterns
that follow.

1.1 The Anatomy of an Object: Classes, Instances, __init__, and self

At its core, OOP is a paradigm for structuring programs by grouping related data and
behaviors into "objects".1 This approach simplifies the modeling of real-world entities,
such as an employee, a car, or a bank account, making code more modular, reusable,
and scalable.1

A class is the blueprint or template from which objects are created. It defines a set of
attributes (data) and methods (behaviors) that all objects of that class will possess.1
An

object, also known as an instance, is a concrete realization of a class, holding its own
specific state.2 For example, if
Dog is a class, then two individual dogs, ozzy and skippy, are instances of that class,
each with its own name and age.3

The __init__ Method: The Object Initializer

When an instance of a class is created, Python automatically calls a special method


named __init__. This method's purpose is to initialize the newly created object's state,
setting the initial values for its attributes.1 It is often referred to as the class
constructor, although its technical role is that of an initializer.

Consider a class to represent an employee in an organization:

Python

class Employee:​
def __init__(self, name, age, position):​
# self.name, self.age, and self.position are instance attributes​
self.name = name​
self.age = age​
self.position = position​

In this example, when a new Employee object is created (e.g., emp1 =


Employee("Alice", 30, "Software Engineer")), the __init__ method is invoked. It takes
the provided arguments ("Alice", 30, "Software Engineer") and assigns them to the
attributes of the specific instance being created.1

The self Parameter: A Reference to the Instance

The self parameter, which is the first argument of any instance method including
__init__, is a reference to the current instance of the class.2 Through

self, a method can access and modify the attributes and other methods of that
specific object. For instance, self.name = name assigns the value of the name
parameter to the name attribute of the particular Employee instance being created.1
While it must be explicitly listed as the first parameter in the method definition, Python
passes it automatically when the method is called on an instance (e.g.,

ozzy.bark() implicitly passes ozzy as the self argument).3

Beyond Initialization: __init__ vs. __new__

For an SDE-I candidate, distinguishing between __init__ and __new__ demonstrates a


more profound understanding of Python's object creation process. While __init__
initializes an object's state, __new__ is the special method responsible for creating
and returning the instance itself. It is a static method that is called before __init__.

The sequence is as follows:


1.​ __new__ is called to create a new, empty instance of the class.
2.​ __new__ returns this new instance.
3.​ The returned instance is then passed as the self argument to the __init__ method
for initialization.

This distinction is not merely academic; it is critical for implementing certain advanced
design patterns. For example, the Singleton pattern, which ensures a class has only
one instance, is often implemented by overriding __new__. The custom __new__
method checks if an instance already exists. If it does, it returns the existing instance
instead of creating a new one, effectively bypassing the normal creation process.4
Understanding this allows a candidate to explain precisely

why they would override __new__ (to control instance creation) versus __init__ (to
control instance state), showcasing a mastery of the object model.

1.2 State and Behavior: Instance vs. Class Variables and Methods

Objects encapsulate both state (what they know) and behavior (what they can do). In
Python, this is represented by variables and methods, which can belong either to an
individual instance or to the class as a whole.

Instance Variables are unique to each object. They represent the specific state of an
instance and are defined within methods (typically __init__) using the self keyword,
such as self.name.2 Modifying an instance variable on one object has no effect on
other objects of the same class. For example, changing

ozzy.age does not change skippy.age.3

Class Variables are shared across all instances of a class. They are defined directly in
the class scope, outside of any method.2 They represent properties that are common
to all objects of that type. For example:

Python

class Dog:​
species = "Canis familiaris" # Class variable​

def __init__(self, name, age):​
self.name = name # Instance variable​
self.age = age # Instance variable​

Here, species is the same for all Dog objects and can be accessed via the class
(Dog.species) or any instance (ozzy.species). If the class variable is changed
(Dog.species = "Canis lupus familiaris"), the change is reflected in all instances that
have not explicitly overridden it.2

Just as there are two types of variables, there are three types of methods, and
choosing the correct one is a design decision that reflects the method's purpose.6
●​ Instance Methods: These are the most common methods. They must have self
as their first parameter and operate on the state of a specific instance.3 For
example, a​
birthday() method that increments self.age is an instance method because it
modifies a specific dog's age.3
●​ Class Methods (@classmethod): These methods are bound to the class, not the
instance. They take the class itself as the first argument, conventionally named
cls. They can be used to access or modify class state (like the species variable)
or, more commonly, as alternative constructors.6 For example, a​
@classmethod could create a Dog instance from a birth year instead of an age.
This is a form of the Factory Method pattern.
●​ Static Methods (@staticmethod): These methods are not bound to the instance
or the class. They do not receive self or cls as an implicit first argument and are
essentially regular functions namespaced within the class.6 They cannot modify
instance or class state. They are used for utility functions that are logically related
to the class but do not depend on its state. For example, a​
Dog class might have a @staticmethod called is_valid_name(name) to check if a
given name string meets certain criteria.

Being able to articulate the rationale for choosing one method type over another—for
instance, using a @classmethod for a factory to ensure correct instantiation in
subclasses, or a @staticmethod for a stateless utility function—demonstrates a
thoughtful approach to design that interviewers value.

1.3 The Four Pillars of OOP

Object-Oriented Programming is built upon four conceptual pillars that enable robust
and scalable software design.1

Encapsulation

Encapsulation is the practice of bundling data (attributes) and the methods that
operate on that data into a single unit, or class.1 Its primary goal is to protect an
object's internal state from outside interference and misuse. By exposing well-defined
methods to interact with the object's data, encapsulation helps maintain data integrity
and promotes modularity.1

In Python, encapsulation is enforced by convention rather than strict access control.


●​ Public: By default, all attributes and methods are public and can be accessed
from anywhere.2
●​ Protected (_ prefix): A single underscore prefix (e.g., self._age) is a convention
that signals to other developers that an attribute or method is intended for
internal use by the class and its subclasses. However, the language does not
prevent external access.7
●​ Private (__ prefix): A double underscore prefix (e.g., self.__id) triggers name
mangling. Python changes the name of the attribute to
_ClassName__attributeName, making it difficult (though not impossible) to access
from outside the class.7 This is used to prevent accidental modification of internal
state by subclasses or external clients.

Inheritance

Inheritance allows a new class (the subclass or child class) to acquire the attributes
and methods of an existing class (the superclass or parent class).1 This is a
fundamental mechanism for code reuse and for creating hierarchical relationships
between classes. The subclass can extend the parent's functionality by adding new
methods or override existing ones to provide more specific behavior.

The super() function is a key tool in inheritance. It allows a subclass to call methods
from its parent class.1 This is crucial when a subclass needs to extend, rather than
completely replace, a parent's method. For example, a subclass's

__init__ method should typically call super().__init__(...) to ensure the parent class is
properly initialized.

Abstraction

Abstraction means hiding complex implementation details and exposing only the
essential, high-level functionalities of an object.1 It simplifies the interaction with
objects by providing a clean and simple interface, allowing developers to focus on

what an object does rather than how it does it.

In Python, abstraction is formally implemented using Abstract Base Classes (ABCs)


from the abc module. An ABC can define a common interface for a set of subclasses.
By using the @abstractmethod decorator, an ABC can declare methods that must be
implemented by any concrete subclass that inherits from it. This enforces a contract,
ensuring that all subclasses provide a certain set of behaviors, which is foundational
for principles like OCP and DIP.

Polymorphism

Polymorphism, meaning "many forms," is the ability of an object to be treated as an


instance of its parent class. It allows a single interface (like a method name) to
represent different underlying forms (implementations).1 In Python, polymorphism is
often achieved through

duck typing: "If it walks like a duck and quacks like a duck, then it must be a duck."
This means Python doesn't strictly check the type of an object; it only cares if the
object can perform the requested operation (i.e., if it has the required method or
attribute).1

For example, if you have a function make_it_speak(animal), you can pass it any object
that has a speak() method, regardless of whether it's a Dog, Cat, or Duck object.
Method overriding in inheritance is another classic example of polymorphism, where a
subclass provides a specific implementation of a method that is already defined in its
superclass.

Part 2: The Principles of Enduring Design - Applying SOLID

Moving from the syntax of OOP to the art of software design requires a set of guiding
principles. The SOLID principles, compiled by Robert C. Martin, are a cornerstone of
modern object-oriented design.8 They are not rigid laws but heuristics that, when
applied, lead to systems that are more understandable, flexible, maintainable, and
testable.9 For an SDE-I, demonstrating an understanding of these principles shows a
commitment to writing professional-quality code. Each principle will be explained with
a clear "before" (violation) and "after" (adherence) example in Python.

Principle Acronym Core Idea


Single Responsibility Principle S A class should have only one
reason to change.10

Open-Closed Principle O Open for extension, but


closed for modification.8

Liskov Substitution Principle L Subtypes must be


substitutable for their base
types.11

Interface Segregation I Clients should not be forced


Principle to depend on methods they
do not use.8

Dependency Inversion D Depend on abstractions, not


Principle on concretions.8

2.1 (S) The Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) states that a class should have one, and only
one, reason to change.10 This doesn't mean a class can only do one thing, but rather
that all of its responsibilities should be aligned with a single, cohesive purpose. If a
class takes on multiple, unrelated responsibilities, it becomes tightly coupled; a
change in one responsibility might necessitate a change in the class, potentially
breaking its other, unrelated functionalities.12

The key to applying SRP is to think in terms of "reasons for change," which are often
tied to different actors or business concerns. For example, a report generation module
might have two reasons to change: one driven by the content of the report (a request
from the accounting department) and another driven by the format of the report (a
request from the data visualization team). SRP suggests these two concerns should
be separated.

Violation Example:
Consider a FileManager class that manages both reading/writing files and compressing them
into ZIP archives.

Python
# Violation of SRP​
from pathlib import Path​
from zipfile import ZipFile​

class FileManager:​
def __init__(self, filename):​
self.path = Path(filename)​

def read(self, encoding="utf-8"):​
return self.path.read_text(encoding)​

def write(self, data, encoding="utf-8"):​
self.path.write_text(data, encoding)​

def compress(self):​
with ZipFile(self.path.with_suffix(".zip"), mode="w") as archive:​
archive.write(self.path)​

def decompress(self):​
with ZipFile(self.path.with_suffix(".zip"), mode="r") as archive:​
archive.extractall()​

This class has two distinct responsibilities: standard file I/O and ZIP archive
management. A change in the compression library or logic would force a modification
to a class that is also responsible for simple file reading and writing.11

Adherence Example:
To adhere to SRP, we separate these responsibilities into two distinct classes.

Python

# Adherence to SRP​
from pathlib import Path​
from zipfile import ZipFile​

class FileManager:​
def __init__(self, filename):​
self.path = Path(filename)​

def read(self, encoding="utf-8"):​
return self.path.read_text(encoding)​

def write(self, data, encoding="utf-8"):​
self.path.write_text(data, encoding)​

class ZipFileManager:​
def __init__(self, filename):​
self.path = Path(filename)​

def compress(self):​
with ZipFile(self.path.with_suffix(".zip"), mode="w") as archive:​
archive.write(self.path)​

def decompress(self):​
with ZipFile(self.path.with_suffix(".zip"), mode="r") as archive:​
archive.extractall()​

Now, FileManager has only one reason to change (changes in file I/O logic), and
ZipFileManager has only one reason to change (changes in ZIP logic). The classes are
smaller, more focused, and easier to maintain and test.11

2.2 (O) The Open-Closed Principle (OCP)

The Open-Closed Principle (OCP) dictates that software entities (classes, modules,
functions) should be open for extension, but closed for modification.8 This means that
one should be able to add new functionality to a system without altering existing,
tested code. Adhering to OCP reduces the risk of introducing bugs into stable code
and is a hallmark of a mature, scalable architecture.

The primary mechanism for achieving OCP is abstraction. By programming to an


interface (an abstract base class in Python) rather than a concrete implementation,
high-level modules can be closed to modification while new low-level implementations
can be added, extending the system's functionality.
Violation Example:
Consider an AreaCalculator class that calculates the area of different shapes.

Python

# Violation of OCP​
from math import pi​

class AreaCalculator:​
def calculate_area(self, shape, **kwargs):​
ifshape == "rectangle":​
return kwargs["width"] * kwargs["height"]​
elif shape == "circle":​
return pi * kwargs["radius"]**2​
# To add a new shape, like a triangle, we must MODIFY this method.​

This design is not closed for modification. Every time a new shape is introduced, the
calculate_area method must be changed, increasing the chance of errors in the
existing logic.10

Adherence Example:
We refactor the design to use an abstract Shape class.

Python

# Adherence to OCP​
from abc import ABC, abstractmethod​
from math import pi​

class Shape(ABC):​
@abstractmethod​
def calculate_area(self):​
pass​

class Rectangle(Shape):​
def __init__(self, width, height):​
self.width = width​
self.height = height​

def calculate_area(self):​
return self.width * self.height​

class Circle(Shape):​
def __init__(self, radius):​
self.radius = radius​

def calculate_area(self):​
return pi * self.radius**2​

# Now, we can add a new shape without modifying any existing code.​
class Triangle(Shape):​
def __init__(self, base, height):​
self.base = base​
self.height = height​

def calculate_area(self):​
return 0.5 * self.base * self.height​

class AreaCalculator:​
def calculate_total_area(self, shapes: list):​
return sum(shape.calculate_area() for shape in shapes)​

The AreaCalculator now depends on the Shape abstraction. It can work with any
object that conforms to the Shape interface. We can extend the system by adding
Triangle, Square, or any other shape class without ever touching the AreaCalculator
class. The system is open to extension (new shapes) but closed to modification (the
core calculator logic is stable).11

2.3 (L) The Liskov Substitution Principle (LSP)

The Liskov Substitution Principle (LSP) is a more specific definition of a subtype


relationship. It states that if S is a subtype of T, then objects of type T in a program
may be replaced with objects of type S without altering any of the desirable
properties of that program.11 In simpler terms, a subclass should be able to stand in
for its superclass without causing unexpected behavior.

This principle is about behavioral subtyping, not just syntactic. A subclass must not
only implement the methods of its parent but must also adhere to the parent's
behavioral contract, including its invariants (conditions that must always be true),
preconditions, and postconditions.

Violation Example:
The classic example is the Rectangle and Square relationship. Mathematically, a square is a
rectangle. However, modeling this with inheritance can violate LSP.

Python

# Violation of LSP​
class Rectangle:​
def __init__(self, width, height):​
self._width = width​
self._height = height​

def get_width(self):​
return self._width​

def set_width(self, value):​
self._width = value​

def get_height(self):​
return self._height​

def set_height(self, value):​
self._height = value​

def calculate_area(self):​
return self._width * self._height​

class Square(Rectangle):​
def __init__(self, side):​
super().__init__(side, side)​

def set_width(self, value):​
self._width = value​
self._height = value​

def set_height(self, value):​
self._width = value​
self._height = value​

A client function that works with a Rectangle might assume it can set the width and height
independently.
def set_and_check(rect: Rectangle): rect.set_width(5); rect.set_height(4); assert
rect.calculate_area() == 20
This function works for Rectangle but fails for Square, because setting the height also
changes the width. The Square object does not behave like a Rectangle, thus violating LSP.11
Adherence Example:
The correct approach is to recognize that Square and Rectangle do not share a behavioral
is-a relationship in this context. They should not be parent and child. Instead, they can be
siblings inheriting from a common abstraction.

Python

# Adherence to LSP​
from abc import ABC, abstractmethod​

class Shape(ABC):​
@abstractmethod​
def calculate_area(self):​
pass​

class Rectangle(Shape):​
def __init__(self, width, height):​
self.width = width​
self.height = height​

def calculate_area(self):​
return self.width * self.height​

class Square(Shape):​
def __init__(self, side):​
self.side = side​

def calculate_area(self):​
return self.side ** 2​

Now, any code that works with a Shape can correctly handle both Rectangle and
Square objects, as they both fulfill the Shape contract without breaking each other's
behavioral assumptions.11

2.4 (I) The Interface Segregation Principle (ISP)

The Interface Segregation Principle (ISP) states that no client should be forced to
depend on methods that it does not use.8 This principle tackles the problem of "fat"
interfaces—large interfaces with many methods. When a class implements such an
interface, it is forced to provide implementations for all methods, even those it doesn't
need, leading to bloated classes and unnecessary dependencies. ISP advocates for
creating smaller, more cohesive, client-specific interfaces.

ISP can be seen as an application of the Single Responsibility Principle to interfaces. A


fat interface has multiple responsibilities, and thus multiple reasons to change, forcing
changes on all implementing classes.

Violation Example:
Consider a single Machine interface for an all-in-one printer.

Python

# Violation of ISP​
from abc import ABC, abstractmethod​

class Machine(ABC):​
@abstractmethod​
def print_doc(self, document): pass​

@abstractmethod​
def fax_doc(self, document): pass​

@abstractmethod​
def scan_doc(self, document): pass​

class OldFashionedPrinter(Machine):​
def print_doc(self, document):​
print(f"Printing {document}")​

def fax_doc(self, document):​
raise NotImplementedError("This printer cannot fax.")​

def scan_doc(self, document):​
raise NotImplementedError("This printer cannot scan.")​

The OldFashionedPrinter is forced to implement fax_doc and scan_doc, even though it


cannot perform these actions. A client that only needs to print should not have to
know about faxing or scanning.11

Adherence Example:
We segregate the fat interface into smaller, role-based interfaces.

Python

# Adherence to ISP​
from abc import ABC, abstractmethod​

class Printable(ABC):​
@abstractmethod​
def print_doc(self, document): pass​

class Faxable(ABC):​
@abstractmethod​
def fax_doc(self, document): pass​

class Scannable(ABC):​
@abstractmethod​
def scan_doc(self, document): pass​

class OldFashionedPrinter(Printable):​
def print_doc(self, document):​
print(f"Printing {document}")​

class ModernMultiFunctionPrinter(Printable, Faxable, Scannable):​
def print_doc(self, document):​
print(f"Printing {document}")​

def fax_doc(self, document):​
print(f"Faxing {document}")​

def scan_doc(self, document):​
print(f"Scanning {document}")​

Now, OldFashionedPrinter only needs to concern itself with the Printable interface.
ModernMultiFunctionPrinter can implement multiple interfaces to provide its full range
of features. Clients can now depend only on the interfaces they actually need, leading
to a more decoupled and maintainable system.11

2.5 (D) The Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) is a crucial concept for creating loosely
coupled systems. It consists of two parts:
1.​ High-level modules should not depend on low-level modules. Both should depend
on abstractions.
2.​ Abstractions should not depend on details. Details should depend on
abstractions.8

Essentially, DIP inverts the traditional flow of dependency. Instead of a high-level


policy class depending directly on a low-level mechanism class, both now depend on
an intermediate abstraction. This decoupling is the key to building pluggable, testable,
and flexible systems.

Violation Example:
A NotificationService directly creates and uses a concrete EmailClient.

Python
# Violation of DIP​
class EmailClient:​
def send_email(self, recipient, message):​
print(f"Sending email to {recipient}: {message}")​

class NotificationService:​
def __init__(self):​
self.client = EmailClient() # Direct dependency on a concrete class​

def send_notification(self, user, message):​
self.client.send_email(user, message)​

The high-level NotificationService is tightly coupled to the low-level EmailClient. If the


business decides to switch to sending SMS messages instead of emails, the
NotificationService class itself must be modified. This also makes testing difficult, as
you cannot test the service without a real EmailClient.

Adherence Example:
We introduce an abstract MessageClient interface and use dependency injection.

Python

# Adherence to DIP​
from abc import ABC, abstractmethod​

# The Abstraction​
class MessageClient(ABC):​
@abstractmethod​
def send(self, recipient, message):​
pass​

# Low-level details depend on the abstraction​
class EmailClient(MessageClient):​
def send(self, recipient, message):​
print(f"Sending email to {recipient}: {message}")​

class SmsClient(MessageClient):​
def send(self, recipient, message):​
print(f"Sending SMS to {recipient}: {message}")​

# High-level module depends on the abstraction​
class NotificationService:​
def __init__(self, client: MessageClient): # Dependency is injected​
self.client = client​

def send_notification(self, user, message):​
self.client.send(user, message)​

# Client code can now "plug in" any client that adheres to the interface​
email_client = EmailClient()​
notification_service_email = NotificationService(email_client)​
notification_service_email.send_notification("[email protected]", "Hello via Email!")​

sms_client = SmsClient()​
notification_service_sms = NotificationService(sms_client)​
notification_service_sms.send_notification("1234567890", "Hello via SMS!")​

The NotificationService now depends on the MessageClient abstraction, not on any


concrete implementation. The concrete EmailClient and SmsClient also depend on this
abstraction by implementing it. This inversion of dependency makes the system
flexible. We can add new notification methods (e.g., PushNotificationClient) without
changing NotificationService. Furthermore, for testing, we can inject a
MockMessageClient, isolating the service from external dependencies and making
unit tests clean and reliable.11

Part 3: The Toolkit - Essential Design Patterns in Python

While SOLID principles provide the philosophical foundation for good design, design
patterns offer proven, reusable solutions to commonly occurring problems within a
given context.13 For an SDE-I interview, a deep understanding of a few fundamental
patterns is far more valuable than a superficial knowledge of many. This section
focuses on the most critical "Gang of Four" (GoF) patterns, categorized by their
intent: Creational, Structural, and Behavioral.14

Category Pattern Name Problem It Solves


Creational Singleton Ensure a class has only one
instance and provide a global
access point to it.15

Creational Factory Method Defer instantiation to


subclasses, creating objects
without specifying the exact
class.15

Creational Builder Construct a complex object


step-by-step, allowing for
different representations.15

Structural Adapter Allow objects with


incompatible interfaces to
collaborate.16

Structural Decorator Attach additional


responsibilities to an object
dynamically.16

Structural Facade Provide a simplified interface


to a complex subsystem.16

Behavioral Strategy Define a family of algorithms,


encapsulate each one, and
make them interchangeable.17

Behavioral Observer Define a one-to-many


dependency so that when one
object changes state, all its
dependents are notified.17

3.1 Creational Patterns: Controlling Object Creation

Creational patterns abstract the instantiation process, making a system independent


of how its objects are created, composed, and represented.18

3.1.1 The Singleton Pattern


Concept: The Singleton pattern ensures that a class has only one instance and
provides a global point of access to it.15 This is useful for managing shared resources
where having multiple instances would be problematic or inefficient.

Use Cases:
●​ Loggers: A single logging instance to centralize all application logs.21
●​ Database Connections: A single connection pool object to manage connections
to a database.23
●​ Configuration Managers: A single object to hold and provide access to
application-wide settings.22

Python Implementation:
While several methods exist, a thread-safe implementation using a metaclass is robust. A
metaclass defines the behavior of a class, just as a class defines the behavior of an instance.
By controlling the class's __call__ method (which is invoked when you instantiate a class), we
can intercept the creation process.

Python

from threading import Lock​



class SingletonMeta(type):​
"""A thread-safe implementation of the Singleton pattern using a metaclass."""​
_instances = {}​
_lock: Lock = Lock()​

def __call__(cls, *args, **kwargs):​
# Double-checked locking to ensure thread safety efficiently.​
if cls not in cls._instances:​
with cls._lock:​
# Check again inside the lock to prevent race conditions.​
cls not in cls._instances:​
if
instance = super().__call__(*args, **kwargs)​
cls._instances[cls] = instance​
return cls._instances[cls]​

class Logger(metaclass=SingletonMeta):​
def log(self, message):​
print(message)​

# Client Code​
logger1 = Logger()​
logger2 = Logger()​

print(f"Are logger1 and logger2 the same instance? {logger1 is logger2}")​
logger1.log("This is the first log message.")​
logger2.log("This is the second log message from the same logger.")​

This implementation uses a lock to ensure that even in a multithreaded environment,


only one instance is ever created.25

A simpler, more Pythonic approach is to leverage the fact that Python modules are
themselves singletons. When a module is imported for the first time, it is initialized and
cached. Subsequent imports return the cached module object.4

Python

# logger_module.py​
# This module acts as a singleton.​

class Logger:​
def log(self, message):​
print(message)​

# The single instance is created when the module is first imported.​
logger_instance = Logger()​

Pros and Cons:


●​ Pros: Guarantees a single instance, provides a global access point, and allows for
lazy initialization.22
●​ Cons: Often considered an anti-pattern as it introduces global state, which can
lead to tight coupling between components and make unit testing difficult. A class
that depends on a Singleton cannot be tested in isolation.22
3.1.2 The Factory Method Pattern

Concept: The Factory Method pattern defines an interface for creating an object but
lets subclasses decide which class to instantiate. It allows a class to defer instantiation
to its subclasses, decoupling the client code from the concrete product classes.13

Example: Imagine a logistics application. A base Logistics class declares an abstract


create_transport() factory method. A RoadLogistics subclass overrides this method to
create Truck objects, while a SeaLogistics subclass overrides it to create Ship objects.
The client code that plans the delivery works with the Logistics class and doesn't
need to know whether it's dealing with trucks or ships.

Python Implementation (Classic):

Python

from abc import ABC, abstractmethod​



# The Product Interface​
class Transport(ABC):​
@abstractmethod​
def deliver(self):​
pass​

# Concrete Products​
class Truck(Transport):​
def deliver(self):​
return "Delivering by land in a truck."​

class Ship(Transport):​
def deliver(self):​
return "Delivering by sea in a ship."​

# The Creator Interface with the Factory Method​
class Logistics(ABC):​
@abstractmethod​
def create_transport(self) -> Transport:​
pass​

def plan_delivery(self):​
transport = self.create_transport()​
result = f"Logistics: Same planning code, but using {transport.deliver()}"​
return result​

# Concrete Creators​
class RoadLogistics(Logistics):​
def create_transport(self) -> Transport:​
return Truck()​

class SeaLogistics(Logistics):​
def create_transport(self) -> Transport:​
return Ship()​

# Client Code​
road_logistics = RoadLogistics()​
print(road_logistics.plan_delivery()) # Uses a Truck​

sea_logistics = SeaLogistics()​
print(sea_logistics.plan_delivery()) # Uses a Ship​

This classic implementation adheres to the Open-Closed Principle: new transport


types (e.g., AirLogistics creating Plane objects) can be added without modifying the
client code or the base Logistics class.26

Pythonic Alternative (Simple Factory):


In Python, because classes are first-class objects, the full inheritance-based pattern is often
unnecessary. A simpler function or method can act as a factory, often using a dictionary to
map identifiers to classes. This achieves the same decoupling with less boilerplate code.27

Python

def get_localizer(language="English"):​
"""A simple factory function."""​
localizers = {​
"French": FrenchLocalizer,​
"English": EnglishLocalizer,​
"Spanish": SpanishLocalizer,​
}​
return localizers.get(language, EnglishLocalizer)()​

A candidate who can explain the classic pattern and then propose this more direct,
Pythonic alternative demonstrates a deep understanding of both the pattern's intent
and the language's features.

3.1.3 The Builder Pattern

Concept: The Builder pattern separates the construction of a complex object from its
representation, allowing the same construction process to create various
representations.15 It is particularly useful when an object requires many configuration
options or optional parameters, as it avoids a "telescoping constructor" with a long list
of arguments.

Example: Constructing a Pizza. A pizza can have different doughs, sauces, and a
variable number of toppings. Instead of a complex constructor, a PizzaBuilder
provides methods like set_dough(), set_sauce(), and add_topping().

Python Implementation:
The pattern typically involves a Builder interface, one or more ConcreteBuilder classes, the
Product being built, and an optional Director class to orchestrate the construction
sequence.29

Python

from abc import ABC, abstractmethod​



# The Product​
class Pizza:​
def __init__(self):​
self.parts =​
def add(self, part):​
self.parts.append(part)​
def __str__(self):​
return f"Pizza with: {', '.join(self.parts)}"​

# The Builder Interface​
class PizzaBuilder(ABC):​
@property​
@abstractmethod​
def product(self): pass​

@abstractmethod​
def build_dough(self): pass​

@abstractmethod​
def build_sauce(self): pass​

@abstractmethod​
def build_topping(self): pass​

# Concrete Builder​
class MargheritaPizzaBuilder(PizzaBuilder):​
def __init__(self):​
self.reset()​

def reset(self):​
self._product = Pizza()​

@property​
def product(self) -> Pizza:​
product = self._product​
self.reset()​
return product​

def build_dough(self):​
self._product.add("thin crust dough")​

def build_sauce(self):​
self._product.add("tomato sauce")​

def build_topping(self):​
self._product.add("mozzarella cheese and basil")​

# Optional Director​
class Director:​
def __init__(self):​
self._builder = None​

@property​
def builder(self) -> PizzaBuilder:​
return self._builder​

@builder.setter​
def builder(self, builder: PizzaBuilder):​
self._builder = builder​

def make_pizza(self):​
self.builder.build_dough()​
self.builder.build_sauce()​
self.builder.build_topping()​

# Client Code​
director = Director()​
builder = MargheritaPizzaBuilder()​
director.builder = builder​

director.make_pizza()​
pizza = builder.product​
print(pizza) # Output: Pizza with: thin crust dough, tomato sauce, mozzarella cheese and basil​

Method chaining is a common feature of the Builder pattern, providing a fluent API:
builder.build_dough().build_sauce().create().

Pros and Cons:


●​ Pros: Allows step-by-step object creation, enables different representations from
the same construction code, and isolates complex construction logic from the
business logic.31
●​ Cons: Increases complexity by requiring the creation of multiple new classes
(Builder, ConcreteBuilder).31
3.2 Structural Patterns: Assembling Flexible Structures

Structural patterns are concerned with how classes and objects are composed to
form larger structures, while keeping these structures flexible and efficient.16

3.2.1 The Adapter Pattern

Concept: The Adapter pattern acts as a bridge, allowing objects with incompatible
interfaces to collaborate.16 It wraps an existing class (the

Adaptee) with a new interface (the Target) that is expected by the client.

Use Cases:
●​ Integrating a new system with legacy code that uses a different interface.34
●​ Using a third-party library whose interface does not match the one required by
the application.35

Python Implementation:
The Adapter can be implemented using either class inheritance or object composition.
Composition is generally preferred because it is more flexible and aligns with the "favor
composition over inheritance" design principle.32

Python

# Object Adapter using Composition​


class EuropeanSocket:​
"""The Adaptee with an incompatible interface."""​
def voltage(self):​
return 230​
def plug_type(self):​
return "Type C"​

class USPlug:​
"""The Target interface expected by the client."""​
def voltage(self):​
return 120​
def plug_type(self):​
return "Type A"​

class Adapter(USPlug):​
"""The Adapter wraps the Adaptee to make it compatible."""​
def __init__(self, european_socket: EuropeanSocket):​
self.socket = european_socket​

def voltage(self):​
# Adapting the voltage (for illustration)​
return self.socket.voltage() - 110​

# plug_type is already compatible in this case, but could also be adapted.​

# Client Code​
def charge_device(plug: USPlug):​
ifplug.voltage() == 120:​
print(f"Charging device with {plug.plug_type()} plug.")​
else:​
print("Incompatible voltage!")​

us_plug = USPlug()​
charge_device(us_plug) # Works fine​

euro_socket = EuropeanSocket()​
# charge_device(euro_socket) # This would fail due to incompatible interface​

adapter = Adapter(euro_socket)​
charge_device(adapter) # Works, thanks to the adapter​

3.2.2 The Decorator Pattern

Concept: The Decorator pattern allows for the addition of new behaviors to objects
dynamically by placing them inside special wrapper objects that share the same
interface.16 It provides a flexible alternative to subclassing for extending functionality,
as behavior can be added or removed at runtime.

Example: A coffee ordering system. A base SimpleCoffee object can be wrapped (or
"decorated") by a MilkDecorator, and then that combination can be further wrapped
by a SugarDecorator. Each decorator adds to the cost and description of the final
product.37

Python Implementation (GoF Pattern):

Python

from abc import ABC, abstractmethod​



# The Component Interface​
class Coffee(ABC):​
@abstractmethod​
def get_cost(self): pass​

@abstractmethod​
def get_description(self): pass​

# Concrete Component​
class SimpleCoffee(Coffee):​
def get_cost(self):​
return 5​
def get_description(self):​
return "Simple coffee"​

# Base Decorator​
class CoffeeDecorator(Coffee, ABC):​
def __init__(self, coffee: Coffee):​
self._decorated_coffee = coffee​

def get_cost(self):​
return self._decorated_coffee.get_cost()​

def get_description(self):​
return self._decorated_coffee.get_description()​

# Concrete Decorators​
class MilkDecorator(CoffeeDecorator):​
def get_cost(self):​
return super().get_cost() + 2​

def get_description(self):​
return super().get_description() + ", with milk"​

class SugarDecorator(CoffeeDecorator):​
def get_cost(self):​
return super().get_cost() + 1​

def get_description(self):​
return super().get_description() + ", with sugar"​

# Client Code​
my_coffee = SimpleCoffee()​
print(f"{my_coffee.get_description()} costs ${my_coffee.get_cost()}")​

my_coffee = MilkDecorator(my_coffee)​
print(f"{my_coffee.get_description()} costs ${my_coffee.get_cost()}")​

my_coffee = SugarDecorator(my_coffee)​
print(f"{my_coffee.get_description()} costs ${my_coffee.get_cost()}")​

GoF Decorator vs. Python's @ Decorator Syntax:


It is crucial for a candidate to distinguish between the GoF Decorator pattern and Python's @
decorator syntax.
●​ GoF Decorator Pattern: As shown above, this is a structural pattern for wrapping
objects at runtime to add state and behavior. It relies on object composition and a
shared interface.36
●​ Python @ Decorator Syntax: This is syntactic sugar for applying a higher-order
function to a function or class definition at compile time. It is typically used for
adding cross-cutting concerns like logging, timing, or authentication checks to
functions.39

An interviewer asking about "decorators" could mean either. A strong candidate will
clarify: "Are you asking about the GoF Decorator pattern for object composition, or
Python's syntactic decorators for wrapping functions?" This demonstrates precision
and a deep understanding of the language and design concepts.

3.2.3 The Facade Pattern

Concept: The Facade pattern provides a simplified, unified interface to a complex


subsystem of classes, libraries, or frameworks.16 It hides the internal complexity from
the client, providing a convenient entry point for common tasks.

Use Cases:
●​ Providing a simple API for a complex set of classes. For example, an
OrderFulfillmentFacade might hide the complexity of interacting with
InventorySystem, PaymentGateway, and ShippingService.23
●​ Decoupling a client from the internal implementation of a subsystem, making the
system easier to maintain and refactor.41

Python Implementation:

Python

# Complex Subsystem Classes​


class InventorySystem:​
def check_stock(self, product_id):​
print(f"Checking stock for {product_id}")​
return True​

class PaymentGateway:​
def process_payment(self, amount):​
print(f"Processing payment of ${amount}")​
return True​

class ShippingService:​
def arrange_shipping(self, address):​
print(f"Shipping to {address}")​
return "TRACK123"​

# The Facade​
class OnlineStoreFacade:​
def __init__(self):​
self.inventory = InventorySystem()​
self.payment = PaymentGateway()​
self.shipping = ShippingService()​

def place_order(self, product_id, amount, address):​
"""A simplified method for the client."""​
print("--- Placing Order ---")​
if self.inventory.check_stock(product_id):​
if self.payment.process_payment(amount):​
tracking_id = self.shipping.arrange_shipping(address)​
print(f"Order placed successfully! Tracking ID: {tracking_id}")​
return True​
print("Order placement failed.")​
return False​

# Client Code​
facade = OnlineStoreFacade()​
facade.place_order("PROD456", 199.99, "123 Python Lane")​

The client interacts only with the simple place_order method, completely unaware of
the complex coordination happening between the subsystem components.41

3.3 Behavioral Patterns: Managing Object Interactions

Behavioral patterns are concerned with algorithms and the assignment of


responsibilities between objects, focusing on how they communicate and
collaborate.17

3.3.1 The Strategy Pattern

Concept: The Strategy pattern defines a family of algorithms, encapsulates each one
in a separate class, and makes their objects interchangeable.17 This allows the
algorithm to vary independently from the clients that use it. It is a powerful pattern for
adhering to the Open-Closed Principle.

Example: A data compression utility that needs to support multiple compression


algorithms (e.g., ZIP, RAR, 7z). Instead of using a large if/elif/else block in the main
Compressor class, the class can hold a reference to a CompressionStrategy object.
The client can then set this strategy to ZipStrategy, RarStrategy, etc., at runtime.45

Python Implementation:

Python

from abc import ABC, abstractmethod​


from typing import List​

# The Strategy Interface​
class SortStrategy(ABC):​
@abstractmethod​
def sort(self, data: List) -> List:​
pass​

# Concrete Strategies​
class AscendingSort(SortStrategy):​
def sort(self, data: List) -> List:​
return sorted(data)​

class DescendingSort(SortStrategy):​
def sort(self, data: List) -> List:​
return sorted(data, reverse=True)​

# The Context​
class Sorter:​
def __init__(self, strategy: SortStrategy):​
self._strategy = strategy​

def set_strategy(self, strategy: SortStrategy):​
self._strategy = strategy​

def execute_sort(self, data: List):​
print(f"Sorting using {self._strategy.__class__.__name__}")​
result = self._strategy.sort(data)​
print(result)​

# Client Code​
numbers = ​
sorter = Sorter(AscendingSort())​
sorter.execute_sort(numbers)​

sorter.set_strategy(DescendingSort())​
sorter.execute_sort(numbers)​

The Sorter (Context) is decoupled from the specific sorting algorithms. New sorting
algorithms can be added simply by creating new classes that implement the
SortStrategy interface, without ever modifying the Sorter class.45

3.3.2 The Observer Pattern

Concept: The Observer pattern (also known as Publish-Subscribe) defines a


one-to-many dependency between objects. When one object (the Subject or
Publisher) changes its state, all its dependents (Observers or Subscribers) are notified
and updated automatically.17

Use Cases:
●​ Event-driven systems: Responding to user actions in a GUI.
●​ Monitoring systems: Notifying administrators when a system metric crosses a
threshold.
●​ Keeping related data in sync: When a change in one data model needs to be
reflected in another.

Python Implementation:

Python
from abc import ABC, abstractmethod​
from typing import List​

# The Observer Interface​
class Observer(ABC):​
@abstractmethod​
def update(self, subject):​
pass​

# The Subject (Publisher)​
class Subject:​
def __init__(self):​
self._observers: List[Observer] =​

def attach(self, observer: Observer):​
if observer not in self._observers:​
self._observers.append(observer)​

def detach(self, observer: Observer):​
self._observers.remove(observer)​

def notify(self):​
for observer in self._observers:​
observer.update(self)​

# Concrete Subject​
class WeatherStation(Subject):​
def __init__(self):​
super().__init__()​
self._temperature = 0​

@property​
def temperature(self):​
return self._temperature​

@temperature.setter​
def temperature(self, value):​
self._temperature = value​
print(f"\nWeatherStation: Temperature changed to {value}°C. Notifying observers.")​
self.notify()​

# Concrete Observers​
class PhoneDisplay(Observer):​
def update(self, subject: WeatherStation):​
print(f"PhoneDisplay: Temperature is now {subject.temperature}°C")​

class WindowSystem(Observer):​
def update(self, subject: WeatherStation):​
ifsubject.temperature > 25:​
print("WindowSystem: It's hot! Closing windows.")​
else:​
print("WindowSystem: Temperature is cool. Opening windows.")​

# Client Code​
station = WeatherStation()​
phone = PhoneDisplay()​
window = WindowSystem()​

station.attach(phone)​
station.attach(window)​

station.temperature = 20​
station.temperature = 30​

The WeatherStation (Subject) does not need to know about the concrete
PhoneDisplay or WindowSystem classes. It only knows that they are Observers. This
loose coupling allows for new types of observers to be added to the system
dynamically without changing the subject.43

Part 4: The Interview Arena - A Strategic Framework for Success

Mastering OOP concepts, SOLID principles, and design patterns is necessary but not
sufficient. An LLD interview is also a test of communication, problem-solving under
pressure, and structured thinking. A candidate who can not only arrive at a good
design but also articulate the process and rationale behind it will stand out. This
section provides a strategic framework for navigating the interview itself.

4.1 The 5-Step LLD Method

A structured approach ensures all requirements are met and demonstrates a


methodical, professional mindset. This 5-step process is a reliable way to tackle any
LLD problem.47

Step 1: Clarify Requirements & Assumptions


This is the most critical step. Never start designing based on assumptions. The initial problem
statement is often intentionally ambiguous to test a candidate's ability to seek clarity.49
●​ Action: Ask clarifying questions to define the scope.
○​ Functional Requirements: What are the core features the system must
have? (e.g., "For a parking lot, can users pay with cash, card, or both?").51
○​ Non-Functional Requirements: Are there constraints related to scale,
performance, or availability? (e.g., "How many vehicles should the system
support simultaneously?").51
○​ Use Cases: Who are the actors (e.g., Admin, Member, System) and what can
they do?.52
●​ Example Questions: For a URL shortener: "What is the expected request rate?
Do URLs expire? Do we need to support custom aliases?".53

Step 2: Define Core Entities & Use Cases


Based on the clarified requirements, identify the main components and interactions of the
system.
●​ Action: List the primary nouns (entities) and verbs (use cases) of the system.
○​ Entities: For a library system, this would be Book, Member, Librarian,
LibraryCard.54
○​ Use Cases: For the same system, this would be checkoutBook, returnBook,
searchBook.54
●​ Goal: This high-level overview sets the stage for the detailed class design and
ensures the core functionality is mapped out before diving into implementation
details.47

Step 3: Design Classes, Attributes, and Relationships


This is the heart of the LLD round, where object-oriented modeling skills are showcased.
●​ Action:
○​ For each entity, define a class. List its key attributes (state) and methods
(behavior).47
○​ Establish the relationships between classes:
■​ Inheritance (is-a): e.g., Car is-a Vehicle.
■​ Composition (owns-a/part-of): e.g., A ParkingLot owns-a collection of
ParkingFloors. The floors cannot exist without the lot.
■​ Aggregation (has-a): e.g., A Department has-a collection of Professors.
The professors can exist independently of the department.
●​ Tools: Use a whiteboard or online editor to sketch a simple UML class diagram.
This visually communicates the structure far more effectively than words alone.48

Step 4: Apply Design Patterns & Principles


As the class structure takes shape, identify opportunities to apply design patterns and SOLID
principles to solve specific problems and improve the design's quality.
●​ Action: Explicitly state the pattern or principle being used and, crucially, why.
●​ Example Justification: "To handle different payment methods for a parking
ticket, I will use the Strategy pattern. I'll define a PaymentStrategy interface with
an executePayment method. This allows us to add new payment types like
CryptoPaymentStrategy in the future without modifying the Ticket class, adhering
to the Open-Closed Principle.".56

Step 5: Discuss Trade-offs, Scalability, and Edge Cases


A senior-level thought process involves recognizing that no design is perfect.
●​ Action:
○​ Trade-offs: Discuss the choices made. "I chose to use a hash map for quick
lookups, which is great for performance but uses more memory. This is a
trade-off between speed and space complexity.".47
○​ Edge Cases: Consider what could go wrong. "What happens if a user tries to
return a book they never checked out? We need to add error handling for this
case.".57
○​ Scalability: Briefly touch on how the design could evolve. "For a single
parking lot, a simple list of available spots is fine. For a nationwide system, we
would need a more sophisticated data structure and possibly a database with
indexing to find the nearest available spot efficiently.".58

4.2 Communicating Your Design: The Art of Thinking Aloud

The interview is a dialogue, not a monologue. The interviewer is assessing a


candidate's thought process, not just the final output.
●​ Narrate Your Process: Continuously verbalize your thoughts. "Okay, first I need
to define the main entities. I'm thinking Vehicle, ParkingSpot, and Ticket are the
most important ones..." This allows the interviewer to follow along and offer
guidance if needed.48
●​ Engage the Interviewer: Treat the interviewer as a collaborator or a senior
engineer on the team. Ask for their input. "I'm considering two approaches here:
inheritance for vehicle types or composition. Do you have a preference or see any
immediate drawbacks to one?" This demonstrates collaboration and
coachability.59
●​ Use Visuals: A picture is worth a thousand words. Use the whiteboard to draw
class diagrams, sequence diagrams, or component interactions. This clarifies
complex relationships and ensures both parties are on the same page.48

4.3 The Amazon Edge: Narrating Your Design Through Leadership Principles

For an Amazon interview, mapping design decisions to their Leadership Principles


(LPs) is a powerful way to demonstrate cultural fit. This goes beyond technical
proficiency and shows an understanding of how Amazon builds products.61
●​ Customer Obsession: This should be the starting point. Frame requirement
clarification questions around the customer's experience.
○​ Narration: "To be customer-obsessed, let's start with the user's journey.
What is the most critical action for them? For a parking lot, it's finding a spot
quickly. My design will prioritize an efficient findAvailableSpot() algorithm."
●​ Invent and Simplify: This principle favors elegant, simple solutions over complex,
over-engineered ones.
○​ Narration: "We could build a complex state machine here, but a simpler
Strategy pattern achieves the same goal with less code. In the spirit of
Invent and Simplify, I'll choose the simpler approach because it's more
maintainable."
●​ Dive Deep: This is demonstrated by thoroughly considering edge cases, error
handling, and potential failure modes.
○​ Narration: "Let's dive deep into what happens during a cash withdrawal at
an ATM. What if the power fails after the bank account is debited but before
the cash is dispensed? Our system needs a transaction log to ensure
atomicity and handle this failure gracefully."
●​ Are Right, A Lot: This is shown by making sound judgments backed by solid
reasoning (i.e., applying SOLID principles and design patterns correctly).
○​ Narration: "I'm making the Square class a sibling of Rectangle rather than a
child to adhere to the Liskov Substitution Principle. This prevents behavioral
inconsistencies and is a more robust design, which is key to being right, a lot
in the long run."
●​ Bias for Action: This can be shown by designing for iterative delivery.
○​ Narration: "My design, which uses the Decorator pattern, supports Bias for
Action. We can launch the coffee ordering system with just SimpleCoffee and
add decorators for Milk and Sugar in subsequent sprints without rewriting the
core logic."

By weaving these LPs into the design narrative, a candidate signals that they are not
just a coder but a potential Amazonian leader who understands the values that drive
the company's engineering culture.

Part 5: LLD in Action - Guided Problem Walkthroughs

This section applies the 5-step strategic framework to several classic LLD interview
questions. Each case study serves as a model answer, demonstrating how to move
from an ambiguous problem statement to a well-designed, well-articulated solution.

5.1 Case Study: Designing a Parking Lot System

This is a quintessential LLD problem that tests core OOP modeling skills.53

Step 1: Clarify Requirements & Assumptions


●​ Vehicle Types: What types of vehicles must be supported? (e.g., Motorcycle, Car,
Bus).63
●​ Spot Types: Do we have different spot sizes? (e.g., Compact, Large,
Handicapped).52 Can a smaller vehicle park in a larger spot? (e.g., Car in a Bus
spot).
●​ Structure: Is it a single lot or multi-level? Multiple entry/exit points?.52
●​ Functionality: The system must handle vehicle entry (issuing a ticket), exit
(calculating and processing payment), and tracking available spots.63
●​ Parking Strategy: How is a spot assigned? First available? Nearest to the
entrance?.56
●​ Payment: What payment methods are supported? (Cash, Credit Card).52

Step 2: Define Core Entities & Use Cases


●​ Entities: ParkingLot, ParkingFloor, ParkingSpot (and subclasses like
CompactSpot, LargeSpot), Vehicle (and subclasses like Car, Motorcycle), Ticket,
PaymentStrategy, ParkingDisplayBoard.
●​ Use Cases: parkVehicle(), unparkVehicle(), calculateFee(), processPayment(),
getFreeSpotsCount().

Step 3: Design Classes, Attributes, and Relationships


●​ Vehicle (Abstract Class):
○​ Attributes: licensePlate, vehicleType (Enum).
○​ Subclasses: Car, Motorcycle, Bus.
●​ ParkingSpot (Abstract Class):
○​ Attributes: spotId, isFree (boolean), spotType (Enum), vehicle (the parked
Vehicle object).
○​ Methods: assignVehicle(), removeVehicle().
○​ Subclasses: CompactSpot, LargeSpot, MotorcycleSpot.
●​ Ticket:
○​ Attributes: ticketId, spotId, entryTime, exitTime, amount.
●​ ParkingFloor:
○​ Attributes: floorId, spots (a list or map of ParkingSpot objects).
●​ ParkingLot (Main Class/Facade):
○​ Attributes: lotId, floors (a list of ParkingFloor objects), tickets (a map to track
active tickets).
○​ Methods: parkVehicle(), unparkVehicle(). This class will contain the core logic.

Step 4: Apply Design Patterns & Principles


●​ Strategy Pattern: To handle different parking strategies. The ParkingLot can have
a parkingStrategy attribute.
○​ ParkingStrategy (Interface): findSpot(vehicleType).
○​ NearestAvailableStrategy (Concrete): Implements logic to find the closest free
spot.
○​ LowestFloorStrategy (Concrete): Implements logic to find a spot on the lowest
possible floor.56
●​ Factory Pattern: A ParkingSpotFactory could be used to create spot objects
based on configuration, simplifying the setup of a ParkingFloor.
●​ Singleton Pattern: The ParkingLot class itself could be a Singleton if the system
is managing only one physical lot, ensuring a single point of control.23
●​ Facade Pattern: The ParkingLot class acts as a facade, simplifying the complex
interactions between floors, spots, and tickets for the client (e.g., an entry
panel).23

Step 5: Discuss Trade-offs, Scalability, and Edge Cases


●​ Concurrency: What if two cars are assigned the same spot simultaneously? The
assignVehicle method on a ParkingSpot must be atomic. This would require a lock
(e.g., threading.Lock in Python) to ensure that only one thread can assign a
vehicle to a spot at a time.
●​ Finding Spots: For a small lot, iterating through a list of spots is fine. For a
massive lot, this is inefficient. A better approach would be to maintain separate
queues or lists of free spots for each vehicle type on each floor. This makes
finding a free spot an O(1) operation.
●​ Edge Cases: Lot is full; invalid ticket presented at exit; vehicle tries to park in a
spot too small for it; payment fails.

5.2 Case Study: Designing a Library Management System

This problem tests the ability to model complex relationships and state transitions.65

Step 1: Clarify Requirements & Assumptions


●​ Actors: Who uses the system? (e.g., Member, Librarian, Admin).65
●​ Items: What can be borrowed? Just books? Are there different types (e.g.,
reference-only)?.54
●​ Core Actions: Search, check-out, return, renew, reserve.66
●​ Rules: What are the limits on checkouts? What are the lending durations? How
are fines calculated?.66
●​ Notifications: How are users notified about due dates or available
reservations?.66

Step 2: Define Core Entities & Use Cases


●​ Entities: Library, Book (the abstract concept), BookItem (the physical copy with a
barcode), Account (base class), Member, Librarian, BookLending (transaction
record), Fine, Notification, Catalog (for searching).
●​ Use Cases: searchByTitle(), checkoutBookItem(), returnBookItem(),
reserveBook(), sendOverdueNotice().

Step 3: Design Classes, Attributes, and Relationships


●​ Account (Abstract Class):
○​ Attributes: id, name, status (Enum: ACTIVE, BLACKLISTED).
○​ Subclasses: Member, Librarian.
●​ Book:
○​ Attributes: ISBN, title, author, subject.
●​ BookItem (extends Book or contains a Book reference):
○​ Attributes: barcode, status (Enum: AVAILABLE, LOANED, RESERVED),
rackLocation. This distinction between Book and BookItem is a key design
point.54
●​ BookLending:
○​ Attributes: transactionId, memberId, bookItemBarcode, issueDate, dueDate.
●​ Catalog:
○​ Attributes: Maps or lists for indexing books by title, author, etc.
○​ Methods: searchByTitle(), searchByAuthor().

Step 4: Apply Design Patterns & Principles


●​ Observer Pattern: This is a perfect fit for handling book reservations.
○​ Subject: The BookItem class. Its state changes from LOANED to AVAILABLE.
○​ Observers: Members who have reserved that book.
○​ Process: When a BookItem is returned, it notifies all observing Members that
it is now available. The first member to act gets it.46
●​ Strategy Pattern: For implementing different search algorithms. The Catalog
could have a searchStrategy that can be set to TitleSearchStrategy or
AuthorSearchStrategy.
●​ Facade Pattern: The Library class can serve as a facade, providing simple
methods like borrowBook(memberId, bookBarcode) that coordinate the
underlying Account, BookItem, and BookLending objects.23

Step 5: Discuss Trade-offs, Scalability, and Edge Cases


●​ Data Storage: For a large library, the catalog's search performance is critical.
This would necessitate database indexing on fields like title and author.65
●​ Reservation Queue: How is the reservation list for a popular book managed? A
FIFO queue is a fair and simple data structure.
●​ Fines: How are fines calculated? Is it a flat rate or per day? The Fine class should
encapsulate this logic.
●​ Edge Cases: Member tries to check out more than the allowed limit; book is lost
or damaged; member is blacklisted but tries to borrow.

5.3 Case Study: Designing an ATM

This problem combines OOP modeling with state management and algorithm design.23

Step 1: Clarify Requirements & Assumptions


●​ Operations: Authenticate user, check balance, deposit cash/checks, withdraw
cash.67
●​ Hardware: The ATM has a card reader, keypad, screen, cash dispenser, and
deposit slot.
●​ Banknotes: The ATM holds a finite number of banknotes of specific
denominations (e.g., $20, $50, $100).67
●​ Withdrawal Logic: Is there a preferred way to dispense cash (e.g., use larger bills
first)?.67
●​ Security: How is the PIN handled?

Step 2: Define Core Entities & Use Cases


●​ Entities: ATM, User, Card, BankAccount, Transaction (and subclasses),
CashDispenser, Screen, Keypad.
●​ Use Cases: authenticateUser(), withdrawCash(), depositFunds(),
displayBalance().

Step 3: Design Classes, Attributes, and Relationships


●​ ATM: The central controller class.
●​ Transaction (Abstract Class):
○​ Attributes: transactionId, date, status.
○​ Subclasses: Withdrawal, Deposit, BalanceInquiry.
●​ CashDispenser:
○​ Attributes: A map of banknote denominations to their counts (e.g., {20: 100,
50: 50}).
○​ Methods: dispenseCash(amount).
●​ State (Interface/Abstract Class): To model the ATM's state.
Step 4: Apply Design Patterns & Principles
●​ State Pattern: The ATM's behavior changes drastically depending on its state.
○​ States: IdleState, HasCardState, AuthenticatedState, OutOfServiceState.
○​ Context: The ATM class.
○​ Process: Instead of a massive if/else in the ATM class, each state object
encapsulates the behavior for that state. For example, in HasCardState,
pressing keys on the keypad enters a PIN. In AuthenticatedState, the same
action selects a transaction type. The ATM delegates actions to its current
state object.46
●​ Command Pattern: To represent user actions.
○​ Commands: WithdrawCommand, DepositCommand.
○​ Invoker: The ATM's keypad/UI.
○​ Receiver: The BankAccount or CashDispenser.
○​ Benefit: This decouples the UI from the execution logic and allows for
logging, queuing, or even undoing commands.17
●​ Chain of Responsibility Pattern: For dispensing cash.
○​ Process: A withdrawal request for a specific amount is passed to the first
handler (e.g., $500_Handler). It dispenses as many $500 bills as it can and
passes the remaining amount to the next handler in the chain
($200_Handler), and so on, until the amount is zero or the request cannot be
fulfilled.

Step 5: Discuss Trade-offs, Scalability, and Edge Cases


●​ Transaction Atomicity (ACID): This is critical. What if the network fails after the
bank is debited but before cash is dispensed? The system must use a robust
transaction management protocol (like two-phase commit) or at least have a
transaction log to reconcile errors.
●​ Security: PINs should never be stored or logged in plain text. They should be
encrypted during transmission.
●​ Hardware Interaction: The LLD should abstract away the hardware. The
CashDispenser class has a dispenseCash method, but we don't need to
implement the low-level mechanical details.
●​ Edge Cases: Insufficient funds in the user's account; insufficient banknotes in the
ATM; card is stolen/expired; user forgets to take their cash.

5.4 Case Study: Designing a Snake and Ladder Game


This is a good question for assessing fundamental OOP skills and logical thinking
without requiring complex patterns.68

Step 1: Clarify Requirements & Assumptions


●​ Board: Standard 10x10 board (100 cells) or a configurable NxN board?.68
●​ Players: How many players?.68
●​ Dice: A single six-sided die?.68
●​ Snakes/Ladders: Are their positions fixed or randomly generated for each game?
●​ Winning Condition: The first player to reach the final cell wins. Must they land
exactly on it? (Rule: If a roll overshoots the final cell, the piece does not move).68

Step 2: Define Core Entities & Use Cases


●​ Entities: Game, Board, Player, Piece, Dice, Snake, Ladder, Cell.
●​ Use Cases: startGame(), rollDice(), movePiece(), playTurn().

Step 3: Design Classes, Attributes, and Relationships


●​ Game:
○​ Attributes: board, players (a queue is good for turn management), dice,
winner.
○​ Methods: startGame(), playTurn(). This class orchestrates the entire game
flow.
●​ Board:
○​ Attributes: size, cells (a list or array), specialObjects (a map from cell number
to a Snake or Ladder object).
○​ Methods: getFinalPosition(start, diceRoll).
●​ Player:
○​ Attributes: name, piece.
●​ Piece:
○​ Attributes: currentPosition.
●​ Dice:
○​ Methods: roll() (returns a random integer from 1 to 6).
●​ Snake / Ladder (could be one BoardEntity class):
○​ Attributes: startPosition, endPosition.

Step 4: Apply Design Patterns & Principles


●​ This problem is more about clean OOP modeling than complex patterns. The
focus should be on Single Responsibility Principle.
○​ Game class is responsible for the game loop and rules.
○​ Board class is responsible for the state of the board.
○​ Player class is responsible for player-specific data.
○​ Dice class is responsible for generating random numbers.
●​ This separation makes the code easy to understand, test, and extend (e.g., adding
a new rule to the Game class doesn't require changing the Board class).

Step 5: Discuss Trade-offs, Scalability, and Edge Cases


●​ Data Structure for Board: A simple array or list of Cell objects is sufficient for
the board itself. For snakes and ladders, a hash map (dict in Python) mapping a
cell's start number to the end number is highly efficient for lookups (O(1)).
●​ Turn Management: A queue (collections.deque in Python) is an excellent choice
for managing player turns. To cycle through turns, you dequeue a player, let them
play, and then enqueue them at the end.
●​ Extensibility: How would the design accommodate a new rule, like "roll a 6 and
get another turn"? This logic would live entirely within the Game class's playTurn
method, demonstrating the benefit of SRP.
●​ Edge Cases: Ensuring snakes and ladders do not form an infinite loop (a
requirement validation step); handling the game end when only one player
remains.68

Conclusion: Beyond the Interview - A Mindset for Clean Code

This playbook provides a comprehensive roadmap for the Python LLD interview. It
begins with the foundational principles of Object-Oriented Programming, builds upon
them with the SOLID principles for robust design, and equips the candidate with a
toolkit of essential design patterns. The true differentiator, however, lies in the ability
to apply this knowledge through a structured, communicative framework, justifying
design decisions not just with technical merit but also with an understanding of
business and user-centric values, as exemplified by Amazon's Leadership Principles.

Mastering these concepts is not merely a means to pass an interview. It is the


foundation of professional software engineering. The practices of writing clean,
maintainable, extensible, and testable code are what distinguish a junior developer
from a senior one. The mindset cultivated through preparing for an LLD
interview—one of thoughtful design, clear communication, and a focus on long-term
value—is the same mindset that leads to building successful, enduring software
systems. The ultimate goal is to internalize these principles so they become a natural
part of the day-to-day development process, long after the interview is over.

Works cited

1.​ Object-Oriented Programming (OOP) in Python – Real Python, accessed July 9,


2025, https://realpython.com/python3-object-oriented-programming/
2.​ Python OOPs Concepts - GeeksforGeeks, accessed July 9, 2025,
https://www.geeksforgeeks.org/python/python-oops-concepts/
3.​ Object-Oriented Programming in Python (OOP): Tutorial - DataCamp, accessed
July 9, 2025, https://www.datacamp.com/tutorial/python-oop-tutorial
4.​ Python Design Patterns in Depth: The Singleton Pattern - Packt, accessed July 9,
2025,
https://www.packtpub.com/en-us/learning/how-to-tutorials/python-design-patte
rns-depth-singleton-pattern
5.​ The Singleton Pattern - Python Design Patterns, accessed July 9, 2025,
https://python-patterns.guide/gang-of-four/singleton/
6.​ Object-Oriented Programming (OOP) (Learning Path) - Real Python, accessed
July 9, 2025,
https://realpython.com/learning-paths/object-oriented-programming-oop-pytho
n/
7.​ Python Object Oriented Programming (OOP) - Full Course for Beginners -
YouTube, accessed July 9, 2025,
https://www.youtube.com/watch?v=iLRZi0Gu8Go
8.​ S.O.L.I.D. Design Principles in Python | by Aserdargun | Medium, accessed July 9,
2025,
https://medium.com/@aserdargun/s-o-l-i-d-design-principles-in-python-e63223
0d6bbe
9.​ What is Low Level Design or LLD? - Learn System Design ..., accessed July 9,
2025,
https://www.geeksforgeeks.org/system-design/what-is-low-level-design-or-lld-l
earn-system-design/
10.​SOLID Design Principles Explained: Building Better Software Architecture -
DigitalOcean, accessed July 9, 2025,
https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-
five-principles-of-object-oriented-design
11.​ SOLID Principles: Improve Object-Oriented Design in Python – Real ..., accessed
July 9, 2025, https://realpython.com/solid-principles-python/
12.​SOLID Principles explained in Python with examples. - GitHub Gist, accessed July
9, 2025, https://gist.github.com/dmmeteo/f630fa04c7a79d3c132b9e9e5d037bfd
13.​Python Design Patterns Tutorial - GeeksforGeeks, accessed July 9, 2025,
https://www.geeksforgeeks.org/python-design-patterns/
14.​Top Design Patterns Interview Questions [2024] - GeeksforGeeks, accessed July
9, 2025,
https://www.geeksforgeeks.org/system-design/top-design-patterns-interview-q
uestions/
15.​Creational Design Patterns - Refactoring.Guru, accessed July 9, 2025,
https://refactoring.guru/design-patterns/creational-patterns
16.​Structural Design Patterns - Refactoring.Guru, accessed July 9, 2025,
https://refactoring.guru/design-patterns/structural-patterns
17.​Behavioral Design Patterns - Refactoring.Guru, accessed July 9, 2025,
https://refactoring.guru/design-patterns/behavioral-patterns
18.​Creational Design Patterns in Python - Stack Abuse, accessed July 9, 2025,
https://stackabuse.com/creational-design-patterns-in-python/
19.​Creational Design Patterns - GeeksforGeeks, accessed July 9, 2025,
https://www.geeksforgeeks.org/system-design/creational-design-pattern/
20.​Complete Guide to Python Design Patterns: Creational, Structural, and
Behavioral, accessed July 9, 2025,
https://www.index.dev/blog/python-design-patterns-complete-guide
21.​Design Patterns in Python and Their Use in Frameworks | by Master Spring Ter,
accessed July 9, 2025,
https://master-spring-ter.medium.com/design-patterns-in-python-and-their-use
-in-frameworks-5dc59f2a7a62
22.​Singleton: Design Pattern in Python | by Minu Kumari - Medium, accessed July 9,
2025,
https://medium.com/@minuray10/singleton-design-pattern-in-python-47d90fd27
365
23.​Five Design Patterns For Your Upcoming Interview | by S Sivaraman - Medium,
accessed July 9, 2025,
https://medium.com/@sivaramansankar2019/five-design-patterns-for-your-upco
ming-interview-af24d87ec29d
24.​Singleton Pattern in Python - A Complete Guide - GeeksforGeeks, accessed July
9, 2025,
https://www.geeksforgeeks.org/python/singleton-pattern-in-python-a-complete-
guide/
25.​Singleton in Python / Design Patterns - Refactoring.Guru, accessed July 9, 2025,
https://refactoring.guru/design-patterns/singleton/python/example
26.​Factory Method in Python / Design Patterns - Refactoring.Guru, accessed July 9,
2025, https://refactoring.guru/design-patterns/factory-method/python/example
27.​Factory Method - Python Design Patterns - GeeksforGeeks, accessed July 9,
2025,
https://www.geeksforgeeks.org/python/factory-method-python-design-patterns/
28.​The Factory Method Pattern - Python Design Patterns, accessed July 9, 2025,
https://python-patterns.guide/gang-of-four/factory-method/
29.​Builder in Python / Design Patterns - Refactoring.Guru, accessed July 9, 2025,
https://refactoring.guru/design-patterns/builder/python/example
30.​Builder Design Pattern - GeeksforGeeks, accessed July 9, 2025,
https://www.geeksforgeeks.org/system-design/builder-design-pattern/
31.​Builder Method - Python Design Patterns - GeeksforGeeks, accessed July 9,
2025,
https://www.geeksforgeeks.org/python/builder-method-python-design-patterns/
32.​Structural Design Patterns in Python - Stack Abuse, accessed July 9, 2025,
https://stackabuse.com/structural-design-patterns-in-python/
33.​Adapter Method - Python Design Patterns - GeeksforGeeks, accessed July 9,
2025,
https://www.geeksforgeeks.org/python/adapter-method-python-design-patterns
/
34.​Adapter in Python / Design Patterns - Refactoring.Guru, accessed July 9, 2025,
https://refactoring.guru/design-patterns/adapter/python/example
35.​Adapter Design Pattern - GeeksforGeeks, accessed July 9, 2025,
https://www.geeksforgeeks.org/system-design/adapter-pattern/
36.​Decorator in Python / Design Patterns - Refactoring.Guru, accessed July 9, 2025,
https://refactoring.guru/design-patterns/decorator/python/example
37.​Exploring the Decorator Pattern in Python | CodeSignal Learn, accessed July 9,
2025,
https://codesignal.com/learn/courses/structural-patterns-in-python/lessons/explo
ring-the-decorator-pattern-in-python
38.​The Decorator Pattern - Python Design Patterns, accessed July 9, 2025,
https://python-patterns.guide/gang-of-four/decorator-pattern/
39.​How to Use Python Decorators (With Function and Class-Based Examples) -
DataCamp, accessed July 9, 2025,
https://www.datacamp.com/tutorial/decorators-python
40.​Primer on Python Decorators, accessed July 9, 2025,
https://realpython.com/primer-on-python-decorators/
41.​Facade in Python / Design Patterns - Refactoring.Guru, accessed July 9, 2025,
https://refactoring.guru/design-patterns/facade/python/example
42.​Facade Pattern in Design Patterns - Tutorialspoint, accessed July 9, 2025,
https://www.tutorialspoint.com/design_pattern/facade_pattern.htm
43.​Design patterns in Python: Behavioral Patterns | by Stanislav ..., accessed July 9,
2025,
https://staskoltsov.medium.com/design-patterns-in-python-behavioral-patterns-
97a2ad221f0e
44.​Behavioral Design Patterns in Python - Stack Abuse, accessed July 9, 2025,
https://stackabuse.com/behavioral-design-patterns-in-python/
45.​Strategy in Python / Design Patterns - Refactoring.Guru, accessed July 9, 2025,
https://refactoring.guru/design-patterns/strategy/python/example
46.​Behavioral Design Patterns - GeeksforGeeks, accessed July 9, 2025,
https://www.geeksforgeeks.org/system-design/behavioral-design-patterns/
47.​How to answer LLD questions in interview? - Design Gurus, accessed July 9,
2025,
https://www.designgurus.io/answers/detail/how-to-answer-lld-questions-in-inter
view
48.​How to Prepare for Low-Level Design Interviews? - GeeksforGeeks, accessed
July 9, 2025,
https://www.geeksforgeeks.org/system-design/how-to-prepare-for-low-level-de
sign-interviews/
49.​Strategy to solve low level design questions? : r/leetcode - Reddit, accessed July
9, 2025,
https://www.reddit.com/r/leetcode/comments/1bifk6h/strategy_to_solve_low_leve
l_design_questions/
50.​Low-Level Design Interview Preparation Roadmap — OOP-Based Languages -
Medium, accessed July 9, 2025,
https://medium.com/@roopa.kushtagi/low-level-design-interview-preparation-ro
admap-oop-based-languages-80364eaf8f2d
51.​How to write a good LLD? - Design Gurus, accessed July 9, 2025,
https://www.designgurus.io/answers/detail/how-to-write-a-good-lld
52.​Design a Parking Lot - Design Gurus, accessed July 9, 2025,
https://www.designgurus.io/course-play/grokking-the-object-oriented-design-int
erview/doc/design-a-parking-lot
53.​25 Low-Level Design Interview Questions You Must Know - Final Round AI,
accessed July 9, 2025,
https://www.finalroundai.com/blog/low-level-design-interview-questions
54.​Design a Library Management System | by Shivam Sinha - Medium, accessed July
9, 2025,
https://shivam-sinha.medium.com/design-a-library-management-system-5f178e
4ce3ce
55.​What is expected in an LLD round? - Design Gurus, accessed July 9, 2025,
https://www.designgurus.io/answers/detail/what-is-expected-in-an-lld-round
56.​Low Level Design of a Parking Lot in Python using Strategy Design Pattern -
Medium, accessed July 9, 2025,
https://medium.com/@prashant558908/low-level-design-of-a-parking-lot-in-pyt
hon-using-strategy-design-pattern-2d0ca9a1c8d1
57.​How I Mastered Low Level Design Interviews - YouTube, accessed July 9, 2025,
https://www.youtube.com/watch?v=OhCp6ppX6bg&pp=0gcJCfwAo7VqN5tD
58.​Can someone help me on what to expect in LLD interviews at Amazon? :
r/leetcode - Reddit, accessed July 9, 2025,
https://www.reddit.com/r/leetcode/comments/1jblada/can_someone_help_me_on
_what_to_expect_in_lld/
59.​How To Communicate In System Design Interview - Easy Climb Tech, accessed
July 9, 2025,
https://easyclimb.tech/learning/articles/how-to-communicate-in-system-design-i
nterview
60.​How to crack an LLD interview? - Design Gurus, accessed July 9, 2025,
https://www.designgurus.io/answers/detail/how-to-crack-an-lld-interview
61.​Top 30 Most Common Amazon Sde 1 Interview Questions You Should Prepare
For, accessed July 9, 2025,
https://www.vervecopilot.com/interview-questions/top-30-most-common-amaz
on-sde-1-interview-questions-you-should-prepare-for
62.​Amazon Software Development Engineer Interview (questions ..., accessed July 9,
2025,
https://igotanoffer.com/blogs/tech/amazon-software-development-engineer-inte
rview
63.​Low-Level Design Interview Question: Parking System | by Mehar Chand |
Medium, accessed July 9, 2025,
https://medium.com/@mehar.chand.cloud/low-level-design-interview-question-p
arking-system-a041bd1973d2
64.​Design a Parking Lot | Machine Coding Round Questions (SDE I/II) - work@tech,
accessed July 9, 2025,
https://workat.tech/machine-coding/practice/design-parking-lot-qm6hwq4wkhp
8
65.​What is the example of low level design? - Design Gurus, accessed July 9, 2025,
https://www.designgurus.io/answers/detail/what-is-the-example-of-low-level-de
sign
66.​Design a Library Management System - Design Gurus, accessed July 9, 2025,
https://www.designgurus.io/course-play/grokking-the-object-oriented-design-int
erview/doc/design-a-library-management-system
67.​Design an ATM Machine - LeetCode, accessed July 9, 2025,
https://leetcode.com/problems/design-an-atm-machine/
68.​kshitijmishra23/Snake-and-Ladder: Low level design (LLD ... - GitHub, accessed
July 9, 2025, https://github.com/kshitijmishra23/Snake-and-Ladder

You might also like