OOP & LLD Cheat Sheet

OOP, SOLID and patterns in 20 topics⏱ 11 min readUpdated

Object-oriented basics, the five SOLID principles and the most asked design patterns, each with a tiny Java example, plus practice low-level design problems.

0 of 22 done

Low-level design rounds check whether you can turn a vague problem into clean classes with clear responsibilities. Interviewers look for solid OOP fundamentals, SOLID thinking and sensible use of a few classic patterns, not for the most clever code.

Read each item, type the small Java example yourself and explain it in your own words. Then use the practice problems at the end to apply everything in a timed setting.

OOP fundamentals

The four pillars and how they fit together.

  1. 1Classes & objectsOOP

    A class is a blueprint that bundles state (fields) and behaviour (methods); an object is one instance of it with its own state. Constructors put a new object into a valid starting state.

    java
    class Account {
        private final String owner;
        private double balance;
        Account(String owner) { this.owner = owner; }
        void deposit(double amt) { balance += amt; }
        double balance() { return balance; }
    }
    // var a = new Account("Asha"); a.deposit(500);
    
  2. 2EncapsulationOOP

    Hide internal state behind private fields and expose only methods that keep it valid. Callers cannot put the object into a broken state, and you can change the internals without breaking them.

    • Make fields private and final where possible
    • Validate in methods, not in callers
    • Avoid setters that just mirror every field
    java
    class Wallet {
        private long paise;                        // hidden state
        public void add(long amount) {
            if (amount <= 0) throw new IllegalArgumentException("amount must be > 0");
            paise += amount;
        }
        public long balance() { return paise; }    // read-only view
    }
    
  3. 3AbstractionOOP

    Expose what an object does and hide how it does it. Callers depend on a simple interface, while messy details such as network calls or retries stay inside the implementation.

    java
    interface PaymentGateway {
        boolean charge(String userId, long amount);   // what, not how
    }
    class UpiGateway implements PaymentGateway {
        public boolean charge(String userId, long amount) {
            // HTTP calls, retries and signatures stay hidden here
            return true;
        }
    }
    
  4. 4InheritanceOOP

    A subclass reuses and specialises a parent class, modelling an is-a relationship. Use it for genuine hierarchies; deep or forced hierarchies make code fragile.

    • Java allows single class inheritance, many interfaces
    • super(...) calls the parent constructor
    • Ask: is a Car always a Vehicle? If not, compose instead
    java
    abstract class Vehicle {
        protected final String plate;
        Vehicle(String plate) { this.plate = plate; }
        abstract int wheels();
    }
    class Car extends Vehicle {
        Car(String plate) { super(plate); }
        int wheels() { return 4; }
    }
    
  5. 5PolymorphismOOP

    One interface, many forms. Overloading means same method name with different parameters, resolved at compile time. Overriding means a subclass replaces a parent method, chosen at runtime from the actual object type.

    • Overriding needs the same signature; use @Override
    • Return type alone cannot overload a method
    • Static and private methods are not overridden
    java
    class Shape { double area() { return 0; } }
    class Circle extends Shape {
        final double r; Circle(double r) { this.r = r; }
        @Override double area() { return Math.PI * r * r; }     // overriding: runtime
    }
    class Calc {
        static int add(int a, int b) { return a + b; }          // overloading:
        static double add(double a, double b) { return a + b; } // compile time
    }
    // Shape s = new Circle(2); s.area();  -> runs Circle.area()
    
  6. 6Interface vs abstract classOOP

    An interface is a pure capability contract a class can implement many of; since Java 8 it may carry default methods but no instance state. An abstract class is a partial base with fields and constructors; a class can extend only one.

    • Interface: can-do (Flyable, Comparable)
    • Abstract class: is-a with shared state or code
    • Default to interfaces; add an abstract base for shared logic
    java
    interface Flyable { void fly(); default String medium() { return "air"; } }
    abstract class Bird {
        protected int legs = 2;                    // state is allowed
        abstract String sound();
        void eat() { System.out.println("pecking"); }
    }
    class Sparrow extends Bird implements Flyable {
        String sound() { return "chirp"; }
        public void fly() { System.out.println("flap"); }
    }
    
  7. 7Composition over inheritanceOOP

    Build behaviour by holding other objects (has-a) instead of extending classes (is-a). Parts can be swapped at runtime and tested alone, and you avoid fragile base-class chains.

    java
    interface Engine { void start(); }
    class PetrolEngine implements Engine { public void start() { System.out.println("vroom"); } }
    class ElectricEngine implements Engine { public void start() { System.out.println("hum"); } }
    class Car {
        private final Engine engine;               // has-a, swappable
        Car(Engine engine) { this.engine = engine; }
        void drive() { engine.start(); }
    }
    // new Car(new ElectricEngine()).drive();
    

SOLID principles

Five rules that keep classes easy to change.

  1. 8S: Single ResponsibilitySOLID

    A class should have one reason to change. Split calculation, persistence and presentation so a change in tax rules never touches database or printing code.

    java
    record Invoice(String id, double total) {}
    class InvoiceCalculator { double tax(Invoice i) { return i.total() * 0.18; } }
    class InvoiceRepository { void save(Invoice i) { /* DB code only */ } }
    class InvoicePrinter { String print(Invoice i) { return i.id() + ": " + i.total(); } }
    // Each class changes for exactly one reason.
    
  2. 9O: Open/ClosedSOLID

    Classes should be open for extension but closed for modification. Add new behaviour by writing a new implementation of an interface instead of editing a growing if-else chain.

    java
    interface Discount { double apply(double price); }
    record FestiveDiscount() implements Discount { public double apply(double p) { return p * 0.9; } }
    record NoDiscount() implements Discount { public double apply(double p) { return p; } }
    class Checkout {
        double total(double price, Discount d) { return d.apply(price); }
    }
    // A new offer is a new class; Checkout never changes.
    
  3. 10L: Liskov SubstitutionSOLID

    Any subclass must be usable wherever its parent is expected without surprising the caller. If a subtype weakens a promise or throws for an inherited method, the hierarchy is wrong.

    • Classic smell: Square extends Rectangle
    • Another: overriding a method only to throw UnsupportedOperationException
    java
    interface Shape { int area(); }
    record Rect(int w, int h) implements Shape { public int area() { return w * h; } }
    record Square(int side) implements Shape { public int area() { return side * side; } }
    // Bad: Square extends Rect with a setWidth() that also changes height,
    // breaking callers that expect setWidth() to leave height alone.
    
  4. 11I: Interface SegregationSOLID

    Prefer several small, focused interfaces over one fat one, so classes never implement methods they do not need. A basic printer should not be forced to implement scan or fax.

    java
    interface Printer { void print(String doc); }
    interface Scanner { String scan(); }
    class BasicPrinter implements Printer {
        public void print(String doc) { System.out.println(doc); }
    }
    class OfficeMachine implements Printer, Scanner {
        public void print(String doc) { System.out.println(doc); }
        public String scan() { return "scanned page"; }
    }
    
  5. 12D: Dependency InversionSOLID

    High-level code should depend on abstractions, not concrete classes. Pass dependencies in through the constructor so you can swap email for SMS, or use a fake in tests, without touching business logic.

    • Constructor injection is the simplest form
    • Spring and Guice automate the wiring
    java
    interface MessageSender { void send(String to, String msg); }
    class EmailSender implements MessageSender {
        public void send(String to, String msg) { System.out.println("mail to " + to); }
    }
    class OrderService {
        private final MessageSender sender;          // depends on abstraction
        OrderService(MessageSender sender) { this.sender = sender; }
        void place(String user) { sender.send(user, "Order placed"); }
    }
    // new OrderService(new EmailSender()) -- injected from outside
    

Design patterns

The five patterns interviewers ask about most.

  1. 13SingletonPattern

    Guarantees exactly one instance of a class with a global access point, e.g. config or a connection pool. In Java the enum form and the lazy holder idiom are thread-safe without explicit locking.

    • Creational pattern
    • Overuse hides dependencies and hurts testing; prefer injection
    java
    enum AppConfig {
        INSTANCE;
        private final java.util.Map<String, String> props = new java.util.HashMap<>();
        String get(String key) { return props.getOrDefault(key, ""); }
    }
    class Registry {
        private Registry() {}
        private static class Holder { static final Registry I = new Registry(); }
        static Registry getInstance() { return Holder.I; }   // lazy + thread-safe
    }
    
  2. 14FactoryPattern

    Moves object creation behind one method so callers ask for what they need by type or config, not by new ConcreteClass(). Adding a new product touches only the factory.

    java
    interface Notifier { void send(String msg); }
    class SmsNotifier implements Notifier { public void send(String m) { System.out.println("SMS " + m); } }
    class EmailNotifier implements Notifier { public void send(String m) { System.out.println("Mail " + m); } }
    class NotifierFactory {
        static Notifier create(String type) {
            return switch (type) {
                case "sms" -> new SmsNotifier();
                case "email" -> new EmailNotifier();
                default -> throw new IllegalArgumentException(type);
            };
        }
    }
    
  3. 15StrategyPattern

    Wraps interchangeable algorithms behind one interface and lets you swap them at runtime, such as pricing, sorting or routing rules. Replaces long if-else chains on a type flag.

    java
    interface FareStrategy { double fare(double km); }
    class RideService {
        private FareStrategy strategy;
        RideService(FareStrategy s) { strategy = s; }
        void setStrategy(FareStrategy s) { strategy = s; }   // swap at runtime
        double quote(double km) { return strategy.fare(km); }
    }
    // FareStrategy normal = km -> 50 + 12 * km;
    // FareStrategy surge  = km -> (50 + 12 * km) * 1.5;
    // new RideService(normal).quote(10);
    
  4. 16ObserverPattern

    A subject keeps a list of subscribers and notifies all of them when something changes, so publishers and listeners stay decoupled. The in-process version of pub/sub, used for events, UI updates and notifications.

    java
    import java.util.*;
    interface Subscriber { void update(String event); }
    class Channel {
        private final List<Subscriber> subs = new ArrayList<>();
        void subscribe(Subscriber s) { subs.add(s); }
        void publish(String video) { subs.forEach(s -> s.update(video)); }
    }
    // Channel c = new Channel();
    // c.subscribe(e -> System.out.println("Got " + e));
    // c.publish("New video");
    
  5. 17BuilderPattern

    Constructs an object with many optional fields step by step through readable chained calls, ending in an immutable result. Avoids telescoping constructors with long lists of nulls.

    java
    class Pizza {
        private final String size; private final boolean cheese, olives;
        private Pizza(Builder b) { size = b.size; cheese = b.cheese; olives = b.olives; }
        static class Builder {
            private final String size; private boolean cheese, olives;
            Builder(String size) { this.size = size; }
            Builder cheese() { cheese = true; return this; }
            Builder olives() { olives = true; return this; }
            Pizza build() { return new Pizza(this); }
        }
    }
    // Pizza p = new Pizza.Builder("L").cheese().olives().build();
    

Practise an LLD round

Sketch the classes, then solve real problems.

  1. 18UML class diagramsUML

    Quick class diagrams show entities, their fields and methods and how they relate. In an interview, a rough box-and-arrow sketch before coding shows structured thinking.

    • Aggregation: part can exist alone; composition: part dies with the whole
    • Mark multiplicity on lines: 1, 0..1, *
    text
    Car   ───>  Driver     association   (uses / knows about)
    Team  ◇───  Player     aggregation   (has-a, part can live alone)
    House ◆───  Room       composition   (owns, part dies with whole)
    Dog   ───▷  Animal     inheritance   (is-a, hollow triangle at parent)
    Dog   ┄┄┄▷  Pet        realization   (implements an interface)
    Members:  + public   - private   # protected   name: Type
    
  2. 19Design a Parking LotProblem

    The classic LLD warm-up: multiple floors and spot sizes, vehicles of different types, tickets on entry and fees on exit. Tests entity modelling, enums, Strategy for pricing and thread-safe spot allocation.

    • Entities: ParkingLot, Floor, Spot, Vehicle, Ticket, FeeStrategy
    • Clarify: spot types, payment, how many entry gates
    • Next drills: elevator system, LRU cache, vending machine, movie booking
    Practise: Parking Lot →
  3. 20Design SplitwiseProblem

    Track shared expenses between users and groups with equal, exact or percentage splits, then show who owes whom. Tests clean modelling, Strategy for split types and balance simplification.

    • Entities: User, Group, Expense, Split, BalanceSheet
    • Validate that splits add up to the total
    • Bonus: minimise the number of settle-up transactions
    Practise: Splitwise →
  4. 21Design an LRU cacheProblem

    A fixed-capacity cache that evicts the least recently used entry. Combines a hash map with a doubly linked list for O(1) get and put, and is often asked as both LLD and coding.

    Practise: LRU cache →
  5. 22Design an elevator systemProblem

    Several elevators serve floors and must pick which car answers each request. Tests the State pattern, scheduling strategies and handling concurrent requests.

    Practise: Elevator system →

Quick tips

  • Clarify requirements and list the core entities before writing any class.
  • Name responsibilities first, then decide which pattern (if any) fits.
  • Favour interfaces and composition; reach for inheritance only for real is-a.
  • Mention thread safety and extensibility before the interviewer asks.
  • Keep the code compiling and readable; a working simple design beats a clever half-finished one.

Topic list based on the free Everything You Need to Get Placed checklist by Mohammad Fraz. Explanations and examples are original.