System Design & DSA Masterclass Intermediate +200 XP

LLD OOP & Patterns

OOP Core Principles: Beyond Basics

Object-Oriented Programming (OOP) is the core paradigm of LLD. It maps complex systems into cohesive, interactive objects:

  • Encapsulation: Binding the data (state) and methods (behavior) together, restricting direct outside access. Prevents illegal state modifications.
  • Abstraction: Hiding internal complexities and exposing only essential interfaces. Focuses on **what** an object does, rather than **how**.
  • Inheritance: Reusing code via 'is-a' relationships (e.g. `SavingsAccount` inherits from `BankAccount`). Keep hierarchies shallow! Prefer Composition.
  • Polymorphism: The ability of different objects to respond to the same interface call differently at runtime (method overriding).

UML Class Relationships & Lifecycles

Designing clean systems requires establishing precise class relationships. UML outlines three core boundaries:

  • 1. Association (Use-a relationship): A loose connection. Objects have independent lifecycles and merely know about each other (e.g., A `Teacher` can teach multiple `Students`, and vice-versa).
    class Teacher { private $students = []; }
  • 2. Aggregation (Has-a relationship): A weak whole-part connection. The child object can exist independently of the parent container (e.g. A `Department` has `Professors`. If the department is deleted, the professors still exist).
    class Department {
      private $professors = [];
      public function addProfessor(Professor $p) { $this->professors[] = $p; } // Injected!
    }
  • 3. Composition (Part-of relationship): A strong, strict whole-part connection. The child cannot exist without the parent. The parent manages the child's instantiation and destruction (e.g., A `House` has `Rooms`. If the house is demolished, the rooms cease to exist).
    class House {
      private $rooms = [];
      public function __construct() {
        $this->rooms[] = new Room("Kitchen"); // Created internally!
      }
    }

The SOLID Principles Explained

SDE-2 interviews strictly audit your application of the **SOLID Principles** to write extensible, maintainable code:

S - Single Responsibility (SRP): A class should have one, and only one, reason to change. If `Order` saves to a DB, generates PDFs, and sends emails, it violates SRP. Decouple them!
O - Open-Closed (OCP): Software entities should be open for extension, but closed for modification. Use interfaces and strategies instead of massive `if/else` checks.
L - Liskov Substitution (LSP): Subtypes must be substitutable for their base types without breaking behavior. If a `Square` subclass inherits from `Rectangle` and changes both width/height during `setWidth()`, it violates LSP.
I - Interface Segregation (ISP): Clients should not be forced to depend on interfaces they do not use. Prefer many small, focused interfaces over one bloated interface.
D - Dependency Inversion (DIP): Depend on abstractions (interfaces), not on concrete implementations. High-level modules should not depend on low-level modules.

Concurrency in LLD: Core Primitives

Modern SDE-2 interviews focus heavily on concurrent access safety. Be prepared to master these primitives:

  • synchronized Keyword: Restricts access to a method or block, allowing only **one thread** to execute it at a time by securing an intrinsic lock.
  • volatile Keyword: Guarantees that variables are read directly from and written to **main memory**, bypassing CPU caches. Prevents thread-visibility problems.
  • ReadWriteLock & ReentrantLock: Allows high concurrent reads (`readLock()`) while locking exclusively for writes (`writeLock()`). Highly customizable compared to `synchronized`.
  • CountDownLatch: A synchronizer that allows one or more threads to wait until a set of operations being performed in other threads completes (countdown reaches zero).
  • CyclicBarrier: A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point before continuing.

Thread-safe Concurrency: The Producer-Consumer Pattern

The **Producer-Consumer** pattern coordinates threads sharing a finite buffer queue, utilizing thread locks, `wait()`, and `notifyAll()` to prevent race conditions and buffer over/underflows:

class SharedQueue {
    private LinkedList queue = new LinkedList();
    private int capacity = 10;

    public synchronized void produce(int item) throws InterruptedException {
        while (queue.size() == capacity) {
            wait(); // Buffer full - producer waits!
        }
        queue.add(item);
        notifyAll(); // Notify consumers that data is ready
    }

    public synchronized int consume() throws InterruptedException {
        while (queue.size() == 0) {
            wait(); // Buffer empty - consumer waits!
        }
        int item = queue.removeFirst();
        notifyAll(); // Notify producers that space is cleared
        return item;
    }
}

Crucial Design Patterns Matrix

Design patterns are standard, reusable templates to solve recurring LLD bottlenecks. Master these primary categories:

  • Creational (Instantiations):
    • **Singleton**: Ensures only one instance exists (e.g., Thread-safe double-checked lock).
    • **Factory Method**: Delegates object instantiation to subclasses.
    • **Builder**: Step-by-step construction of complex objects.
  • Structural (Composition):
    • **Adapter**: Converts incompatible interfaces so they can collaborate.
    • **Decorator**: Dynamically adds wrapping behaviors to objects without modifying original code.
    • **Facade**: Unified, simplified entry face to complex subsystems.
  • Behavioral (Algorithms & Roles):
    • **Strategy**: Defines a family of interchangeable algorithms, selected at runtime.
    • **Observer**: Triggers dynamic notifications to subscriber objects when states change.
    • **State**: Alter object behavior dynamically when its internal state changes.