OOP & LLD All 23 Java Design Patterns

OOP, SOLID & All Design Patterns

SOLID Principles All 23 GoF Patterns (Creational, Structural, Behavioral) LLD Examples

SOLID Principles

S Single Responsibility

  • A class should have only ONE reason to change
  • Wrong: UserService handles auth, email, and DB
  • Right: AuthService, EmailService, UserRepository separate concerns

O Open/Closed

  • Open for extension, closed for modification
  • Add new behavior by adding new classes, not editing old ones
  • Use interfaces + polymorphism. E.g.: new PaymentMethod new class, not if/else

L Liskov Substitution

  • Subclass must be substitutable for parent without breaking
  • If Square extends Rectangle and overrides setWidth to also set height violates LSP
  • Subclasses should not restrict or change base class contracts

I Interface Segregation

  • Clients should not depend on methods they dont use
  • Split fat interfaces into smaller, focused ones
  • Flyable, Swimmable, Walkable rather than one Animal interface with all

D Dependency Inversion

  • Depend on abstractions (interfaces), not concrete implementations
  • High-level modules should not depend on low-level modules
  • OrderService depends on IPaymentGateway, not on StripePaymentGateway directly
Creational Patterns Object Creation
Creational

Singleton

Very Common

Intent: Ensure a class has only ONE instance; provide global access point.

  • Use when: config manager, connection pool, logger, cache, registry
  • Pitfall: hidden global state, hard to test (mock difficult), can be anti-pattern if overused
// Thread-safe Singleton: Double-Checked Locking
public class DatabaseConnectionPool {
    // volatile ensures visibility across threads
    private static volatile DatabaseConnectionPool instance;
    private final List<Connection> pool;

    private DatabaseConnectionPool() {
        pool = initializePool(10);
    }

    public static DatabaseConnectionPool getInstance() {
        if (instance == null) {                    // First check (no lock)
            synchronized (DatabaseConnectionPool.class) {
                if (instance == null) {            // Second check (with lock)
                    instance = new DatabaseConnectionPool();
                }
            }
        }
        return instance;
    }
}

// Better: Enum Singleton (Bill Pugh, thread-safe, serialization-safe)
public enum AppConfig {
    INSTANCE;
    private final Properties props = loadProperties();
    public String get(String key) { return props.getProperty(key); }
}
// Usage: AppConfig.INSTANCE.get("db.url")
Creational

Factory Method

Very Common

Intent: Define interface for creating object, let subclasses decide which class to instantiate.

  • Use when: exact type of object unknown until runtime; delegate creation to subclasses
  • Real-world: Javas Calendar.getInstance(), JDBC DriverManager.getConnection()
// Product interface
interface Notification {
    void send(String message);
}

// Concrete products
class EmailNotification implements Notification {
    public void send(String message) { System.out.println("Email: " + message); }
}
class SMSNotification implements Notification {
    public void send(String message) { System.out.println("SMS: " + message); }
}
class PushNotification implements Notification {
    public void send(String message) { System.out.println("Push: " + message); }
}

// Factory (could also be static factory method)
class NotificationFactory {
    public static Notification create(String type) {
        return switch (type) {
            case "EMAIL" -> new EmailNotification();
            case "SMS"   -> new SMSNotification();
            case "PUSH"  -> new PushNotification();
            default -> throw new IllegalArgumentException("Unknown type: " + type);
        };
    }
}

// Usage: open/closed - add new type without modifying existing code
Notification n = NotificationFactory.create("EMAIL");
n.send("Hello!");
Creational

Abstract Factory

Intent: Create families of related objects without specifying concrete classes.

  • Difference from Factory Method: produces a FAMILY of related objects; Factory Method produces ONE type
  • Use when: UI toolkit (Windows vs Mac), cloud providers (AWS vs GCP), themes (Dark vs Light)
// Abstract products
interface Button  { void render(); }
interface Checkbox { void render(); }

// Concrete products for Windows
class WindowsButton  implements Button   { public void render() { System.out.println("Windows Button"); } }
class WindowsCheckbox implements Checkbox { public void render() { System.out.println("Windows Checkbox"); } }

// Concrete products for Mac
class MacButton   implements Button   { public void render() { System.out.println("Mac Button"); } }
class MacCheckbox implements Checkbox { public void render() { System.out.println("Mac Checkbox"); } }

// Abstract Factory interface
interface UIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// Concrete factories
class WindowsUIFactory implements UIFactory {
    public Button createButton()   { return new WindowsButton(); }
    public Checkbox createCheckbox() { return new WindowsCheckbox(); }
}
class MacUIFactory implements UIFactory {
    public Button createButton()   { return new MacButton(); }
    public Checkbox createCheckbox() { return new MacCheckbox(); }
}

// Client code doesn't know about Windows or Mac
class Application {
    private final UIFactory factory;
    Application(UIFactory factory) { this.factory = factory; }
    void buildUI() {
        Button btn = factory.createButton();
        Checkbox cb = factory.createCheckbox();
        btn.render(); cb.render();
    }
}
// Usage: new Application(new MacUIFactory()).buildUI();
Creational

Builder

Very Common

Intent: Construct complex objects step by step. Separate construction from representation.

  • Use when: object has many optional params; avoid telescoping constructor anti-pattern
  • Real-world: StringBuilder, Lombok @Builder, AlertDialog.Builder (Android)
public class HttpRequest {
    // Required fields
    private final String url;
    private final String method;
    // Optional fields
    private final Map<String, String> headers;
    private final String body;
    private final int timeoutMs;

    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = Collections.unmodifiableMap(builder.headers);
        this.body = builder.body;
        this.timeoutMs = builder.timeoutMs;
    }

    public static class Builder {
        private final String url;         // required
        private final String method;      // required
        private Map<String, String> headers = new HashMap<>();
        private String body;
        private int timeoutMs = 30_000;

        public Builder(String url, String method) {
            this.url = url;
            this.method = method;
        }
        public Builder header(String k, String v) { headers.put(k, v); return this; }
        public Builder body(String body)           { this.body = body; return this; }
        public Builder timeout(int ms)             { this.timeoutMs = ms; return this; }
        public HttpRequest build()                 { return new HttpRequest(this); }
    }
}

// Usage: fluent, readable, no null params
HttpRequest req = new HttpRequest.Builder("https://api.alation.com", "POST")
    .header("Authorization", "Bearer token123")
    .body("{\"name\":\"test\"}")
    .timeout(5000)
    .build();
Creational

Prototype

Intent: Clone existing objects without depending on their concrete classes.

  • Use when: object creation is expensive (DB/network), need many similar objects with small differences
  • Real-world: Object.clone(), Spring bean scope=prototype, cell division in games
  • Deep vs Shallow clone: deep clone copies all nested objects; shallow copies references only
public class QueryTemplate implements Cloneable {
    private String sql;
    private Map<String, Object> params;
    private int timeoutSeconds;

    public QueryTemplate deepClone() {
        try {
            QueryTemplate clone = (QueryTemplate) super.clone();
            clone.params = new HashMap<>(this.params); // Deep copy map
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

// Prototype Registry pattern
class ShapeRegistry {
    private Map<String, Shape> prototypes = new HashMap<>();

    public void register(String key, Shape shape) {
        prototypes.put(key, shape);
    }
    public Shape get(String key) {
        return prototypes.get(key).deepClone(); // Clone, not original
    }
}

// Usage: create many circles with slight differences without new + setup
Shape circle = registry.get("circle"); // cloned from prototype
circle.setColor("red");
circle.draw();
Structural Patterns Object Composition
Structural

Adapter

Very Common

Intent: Convert incompatible interface into one clients expect. "Wrapper that translates interfaces."

  • Use when: integrating legacy code, third-party libraries with different interface, API migration
  • Real-world: Javas Arrays.asList(), InputStreamReader(InputStream), JDBC adapters
// Target interface (what client expects)
interface DataCatalogConnector {
    List<TableMetadata> getTables(String database);
}

// Adaptee (legacy/third-party with different interface)
class LegacyMetadataService {
    public ResultSet executeQuery(String sql) { /* old API */ return null; }
    public Map<String, Object> getSchemaInfo(String dbName) { return Map.of(); }
}

// Adapter: wraps LegacyMetadataService, implements target interface
class LegacyMetadataAdapter implements DataCatalogConnector {
    private final LegacyMetadataService legacy;

    LegacyMetadataAdapter(LegacyMetadataService legacy) {
        this.legacy = legacy;
    }

    @Override
    public List<TableMetadata> getTables(String database) {
        // Translate: call legacy API, convert response to new format
        Map<String, Object> info = legacy.getSchemaInfo(database);
        return convertToTableMetadata(info); // mapping logic
    }
}

// Client uses target interface only, doesn't know about Legacy
DataCatalogConnector connector = new LegacyMetadataAdapter(new LegacyMetadataService());
List<TableMetadata> tables = connector.getTables("analytics");
Structural

Bridge

Intent: Decouple abstraction from implementation so they can vary independently.

  • Use when: both abstraction and implementation need extension independently
  • Vs Adapter: Bridge designed upfront; Adapter retrofits existing code
  • Real-world: JDBC (SQL abstraction multiple DB drivers), device remotes
// Implementation interface
interface MessageSender {
    void sendMessage(String recipient, String content);
}

// Concrete implementations
class EmailSender implements MessageSender {
    public void sendMessage(String to, String content) {
        System.out.println("Email to " + to + ": " + content);
    }
}
class SlackSender implements MessageSender {
    public void sendMessage(String channel, String content) {
        System.out.println("Slack #" + channel + ": " + content);
    }
}

// Abstraction (references implementation via bridge)
abstract class Notification {
    protected MessageSender sender; // Bridge
    Notification(MessageSender sender) { this.sender = sender; }
    abstract void notify(String recipient, String message);
}

// Refined abstractions
class AlertNotification extends Notification {
    AlertNotification(MessageSender sender) { super(sender); }
    public void notify(String r, String m) {
        sender.sendMessage(r, "[ALERT] " + m);
    }
}
class InfoNotification extends Notification {
    InfoNotification(MessageSender sender) { super(sender); }
    public void notify(String r, String m) {
        sender.sendMessage(r, "[INFO] " + m);
    }
}

// Mix and match independently
new AlertNotification(new SlackSender()).notify("ops-channel", "DB is down!");
new InfoNotification(new EmailSender()).notify("user@co.com", "Report ready");
Structural

Composite

Intent: Compose objects into tree structures. Treat individual objects and groups uniformly.

  • Use when: tree structures (files/folders, org charts, UI components, menus)
  • Key: both Leaf and Composite implement same Component interface
// Component interface (uniform treatment)
interface FileSystemItem {
    String getName();
    long getSize();
    void display(String indent);
}

// Leaf
class File implements FileSystemItem {
    private final String name;
    private final long size;
    File(String name, long size) { this.name = name; this.size = size; }
    public String getName() { return name; }
    public long getSize()   { return size; }
    public void display(String indent) {
        System.out.println(indent + "- " + name + " (" + size + "B)");
    }
}

// Composite (can contain leaves OR other composites)
class Directory implements FileSystemItem {
    private final String name;
    private final List<FileSystemItem> children = new ArrayList<>();
    Directory(String name) { this.name = name; }
    public void add(FileSystemItem item) { children.add(item); }
    public String getName() { return name; }
    public long getSize() {
        return children.stream().mapToLong(FileSystemItem::getSize).sum();
    }
    public void display(String indent) {
        System.out.println(indent + "+ " + name + "/");
        children.forEach(c -> c.display(indent + "  "));
    }
}

// Client treats files and directories uniformly
Directory root = new Directory("src");
root.add(new File("Main.java", 1200));
Directory utils = new Directory("utils");
utils.add(new File("Helper.java", 800));
root.add(utils);
root.display(""); // Works recursively
Structural

Decorator

Very Common

Intent: Add behavior to objects dynamically without altering their class.

  • Use when: add responsibilities to objects at runtime; avoid subclass explosion
  • Real-world: Java I/O streams (BufferedInputStream wraps InputStream), Spring AOP, middleware
interface DataSource {
    void writeData(String data);
    String readData();
}

class FileDataSource implements DataSource {
    private final String filename;
    FileDataSource(String fn) { this.filename = fn; }
    public void writeData(String data) { /* write to file */ }
    public String readData()           { return /* read from file */ "raw data"; }
}

// Base decorator (wraps another DataSource)
abstract class DataSourceDecorator implements DataSource {
    protected final DataSource wrapped;
    DataSourceDecorator(DataSource wrapped) { this.wrapped = wrapped; }
    public void writeData(String data) { wrapped.writeData(data); }
    public String readData()           { return wrapped.readData(); }
}

// Concrete decorators
class EncryptionDecorator extends DataSourceDecorator {
    EncryptionDecorator(DataSource wrapped) { super(wrapped); }
    public void writeData(String data) { super.writeData(encrypt(data)); }
    public String readData()           { return decrypt(super.readData()); }
    private String encrypt(String d)   { return "[ENC]" + d + "[/ENC]"; }
    private String decrypt(String d)   { return d.replace("[ENC]","").replace("[/ENC]",""); }
}
class CompressionDecorator extends DataSourceDecorator {
    CompressionDecorator(DataSource wrapped) { super(wrapped); }
    public void writeData(String data) { super.writeData(compress(data)); }
    public String readData()           { return decompress(super.readData()); }
    private String compress(String d)   { return "ZIP(" + d + ")"; }
    private String decompress(String d) { return d.replace("ZIP(","").replace(")",""); }
}

// Stack decorators at runtime
DataSource source = new CompressionDecorator(
                        new EncryptionDecorator(
                            new FileDataSource("data.bin")));
source.writeData("Hello"); // compressed then encrypted then written
Structural

Facade

Very Common

Intent: Provide a simplified interface to a complex subsystem.

  • Use when: simplify complex API for common use cases; hide subsystem complexity
  • Real-world: Hibernate SessionFactory, SLF4J (facade over log implementations), API Gateway
// Complex subsystem classes
class VideoDecoder {
    VideoFile decode(String filename) { return new VideoFile(filename); }
}
class AudioMixer { 
    AudioTrack fix(VideoFile video) { return new AudioTrack(); } 
}
class BitrateConverter { 
    String convert(VideoFile file, String format) { return format + " version"; } 
}
class CodecFactory {
    Codec extract(VideoFile file) { return new H264Codec(); }
}

// Facade: simple interface over the complex video subsystem
class VideoConverter {
    private final VideoDecoder decoder = new VideoDecoder();
    private final AudioMixer mixer = new AudioMixer();
    private final BitrateConverter bitrateConverter = new BitrateConverter();
    private final CodecFactory codecFactory = new CodecFactory();

    public File convertVideo(String filename, String format) {
        VideoFile file = decoder.decode(filename);
        Codec codec = codecFactory.extract(file);
        AudioTrack audio = mixer.fix(file);
        String result = bitrateConverter.convert(file, format);
        return new File(result + ".mp4");
    }
}

// Client only needs to know about Facade
VideoConverter converter = new VideoConverter();
File mp4 = converter.convertVideo("funny.ogg", "mp4");
Structural

Flyweight

Intent: Use sharing to support large numbers of fine-grained objects efficiently.

  • Key: Separate intrinsic state (shared, immutable) from extrinsic state (unique, passed by client)
  • Use when: millions of similar objects consuming too much memory
  • Real-world: Java String pool, character rendering in text editors, game sprites
// Flyweight: shared, immutable intrinsic state
class TreeType {
    private final String name;     // shared
    private final String color;    // shared  
    private final String texture;  // shared
    
    TreeType(String name, String color, String texture) {
        this.name = name; this.color = color; this.texture = texture;
    }
    void draw(int x, int y) { // x, y are extrinsic - passed in
        System.out.printf("Drawing %s tree at (%d,%d)%n", name, x, y);
    }
}

// Flyweight Factory (cache and reuse)
class TreeTypeFactory {
    private static final Map<String, TreeType> cache = new HashMap<>();
    
    public static TreeType getTreeType(String name, String color, String texture) {
        String key = name + "_" + color;
        return cache.computeIfAbsent(key, k -> new TreeType(name, color, texture));
    }
}

// Tree: only stores extrinsic state + reference to flyweight
class Tree {
    private final int x, y;        // extrinsic (unique per tree)
    private final TreeType type;   // shared flyweight

    Tree(int x, int y, TreeType type) { this.x = x; this.y = y; this.type = type; }
    void draw() { type.draw(x, y); }
}

// Usage: 1 million trees, but only 3 TreeType objects in memory
for (int i = 0; i < 1_000_000; i++) {
    TreeType oak = TreeTypeFactory.getTreeType("Oak", "green", "rough");
    new Tree(rand.nextInt(1000), rand.nextInt(1000), oak).draw();
}
Structural

Proxy

Very Common

Intent: Provide surrogate/placeholder for another object to control access.

  • Types: Virtual (lazy init), Protection (access control), Remote (RPC), Caching, Logging
  • Real-world: Spring @Transactional (proxy), Hibernate lazy loading, Java RMI, JDK Dynamic Proxy
interface DataService {
    String getData(String id);
}

class RealDataService implements DataService {
    public String getData(String id) {
        // Expensive DB/network call
        System.out.println("Fetching " + id + " from DB...");
        return "data_for_" + id;
    }
}

// Caching Proxy
class CachingDataServiceProxy implements DataService {
    private final DataService realService;
    private final Map<String, String> cache = new HashMap<>();
    
    CachingDataServiceProxy(DataService realService) {
        this.realService = realService;
    }
    
    public String getData(String id) {
        if (cache.containsKey(id)) {
            System.out.println("Cache hit for " + id);
            return cache.get(id);
        }
        String result = realService.getData(id); // delegate to real
        cache.put(id, result);
        return result;
    }
}

// Protection Proxy (access control)
class SecureDataServiceProxy implements DataService {
    private final DataService realService;
    private final String userRole;
    
    SecureDataServiceProxy(DataService real, String role) {
        this.realService = real; this.userRole = role;
    }
    
    public String getData(String id) {
        if (!"admin".equals(userRole)) throw new SecurityException("Access denied");
        return realService.getData(id);
    }
}

// Client code same for both proxy and real service
DataService service = new CachingDataServiceProxy(new RealDataService());
service.getData("user-123"); // DB call
service.getData("user-123"); // Cache hit!
Behavioral Patterns Object Communication
Behavioral

Chain of Responsibility

Intent: Pass requests along a chain of handlers. Each handler decides to process or pass forward.

  • Use when: request handled by one of multiple handlers; auth middleware chains, event handling
  • Real-world: Java Servlet Filters, Spring Security filter chain, exception handlers
abstract class RequestHandler {
    protected RequestHandler next;
    
    public RequestHandler setNext(RequestHandler next) {
        this.next = next;
        return next; // allows chaining
    }
    
    public abstract void handle(HttpRequest request);
    
    protected void passToNext(HttpRequest request) {
        if (next != null) next.handle(request);
        else System.out.println("Request rejected: no handler processed it");
    }
}

class AuthHandler extends RequestHandler {
    public void handle(HttpRequest request) {
        if (request.hasValidToken()) {
            System.out.println("Auth: passed");
            passToNext(request);
        } else {
            System.out.println("Auth: rejected - invalid token");
        }
    }
}

class RateLimitHandler extends RequestHandler {
    public void handle(HttpRequest request) {
        if (!isRateLimited(request.getClientId())) {
            System.out.println("RateLimit: passed");
            passToNext(request);
        } else {
            System.out.println("RateLimit: rejected - too many requests");
        }
    }
    private boolean isRateLimited(String id) { return false; }
}

class LoggingHandler extends RequestHandler {
    public void handle(HttpRequest request) {
        System.out.println("Logging request: " + request.getUrl());
        passToNext(request); // always passes
    }
}

// Build chain: Auth RateLimit Logging
RequestHandler chain = new AuthHandler();
chain.setNext(new RateLimitHandler()).setNext(new LoggingHandler());
chain.handle(new HttpRequest("/api/data", "Bearer valid_token"));
Behavioral

Command

Common

Intent: Encapsulate a request as an object. Supports undo, queuing, logging operations.

  • Use when: undo/redo, operation queuing, transaction logs, macro recording
  • Real-world: Java Runnable/Callable (command objects), Spring Batch, Event Sourcing
interface Command {
    void execute();
    void undo();
}

// Receiver
class TextEditor {
    private StringBuilder text = new StringBuilder();
    public void write(String s) { text.append(s); }
    public void deleteLast(int n) { text.delete(text.length()-n, text.length()); }
    public String getText() { return text.toString(); }
}

// Concrete commands
class WriteCommand implements Command {
    private final TextEditor editor;
    private final String text;
    
    WriteCommand(TextEditor editor, String text) {
        this.editor = editor; this.text = text;
    }
    public void execute() { editor.write(text); }
    public void undo()    { editor.deleteLast(text.length()); }
}

// Invoker (tracks command history for undo)
class CommandManager {
    private final Deque<Command> history = new ArrayDeque<>();
    
    public void executeCommand(Command cmd) {
        cmd.execute();
        history.push(cmd);
    }
    public void undo() {
        if (!history.isEmpty()) history.pop().undo();
    }
}

// Usage
TextEditor editor = new TextEditor();
CommandManager manager = new CommandManager();
manager.executeCommand(new WriteCommand(editor, "Hello "));
manager.executeCommand(new WriteCommand(editor, "World"));
System.out.println(editor.getText()); // Hello World
manager.undo();
System.out.println(editor.getText()); // Hello
Behavioral

Iterator

Intent: Provide sequential access to elements without exposing underlying structure.

  • Use when: need to traverse collection without knowing its internal representation
  • Real-world: Java Iterator, Iterable, for-each loop (all collections), database cursor
// Custom tree iterator (in-order traversal)
class BinaryTree<T extends Comparable<T>> implements Iterable<T> {
    private Node<T> root;

    static class Node<T> {
        T value; Node<T> left, right;
        Node(T value) { this.value = value; }
    }

    @Override
    public Iterator<T> iterator() {
        return new InOrderIterator<>(root);
    }

    static class InOrderIterator<T> implements Iterator<T> {
        private final Deque<Node<T>> stack = new ArrayDeque<>();

        InOrderIterator(Node<T> root) { pushLeft(root); }

        private void pushLeft(Node<T> node) {
            while (node != null) { stack.push(node); node = node.left; }
        }

        public boolean hasNext() { return !stack.isEmpty(); }

        public T next() {
            Node<T> node = stack.pop();
            pushLeft(node.right);
            return node.value;
        }
    }
}

// Usage: client uses for-each without knowing about tree structure
BinaryTree<Integer> tree = new BinaryTree<>();
// ... add nodes
for (int val : tree) { System.out.println(val); } // in-order traversal
Behavioral

Mediator

Intent: Reduce direct coupling between objects by routing all communication through a mediator.

  • Use when: too many direct relationships between objects; chat room, event bus, air traffic control
  • Real-world: Spring ApplicationEventPublisher, chat application, MVC Controller
interface ChatMediator {
    void sendMessage(String message, User sender);
    void addUser(User user);
}

class ChatRoom implements ChatMediator {
    private List<User> users = new ArrayList<>();
    
    public void addUser(User user) { users.add(user); }
    
    public void sendMessage(String message, User sender) {
        users.stream()
             .filter(u -> u != sender)  // don't send to self
             .forEach(u -> u.receive(message, sender.getName()));
    }
}

abstract class User {
    protected ChatMediator mediator;
    protected String name;
    User(ChatMediator mediator, String name) {
        this.mediator = mediator; this.name = name;
    }
    public String getName() { return name; }
    public void send(String message)              { mediator.sendMessage(message, this); }
    public abstract void receive(String msg, String from);
}

class ChatUser extends User {
    ChatUser(ChatMediator mediator, String name) { super(mediator, name); }
    public void receive(String msg, String from) {
        System.out.println(name + " received from " + from + ": " + msg);
    }
}

// Users don't know about each other - all through mediator
ChatRoom room = new ChatRoom();
User alice = new ChatUser(room, "Alice");
User bob   = new ChatUser(room, "Bob");
room.addUser(alice); room.addUser(bob);
alice.send("Hello everyone!"); // bob receives, not alice
Behavioral

Memento

Intent: Capture and restore object state without violating encapsulation.

  • Use when: undo/redo, snapshots, save/restore game state, transaction rollback
  • Different from Command: stores state (not operation); Command stores operation
// Originator: object whose state we want to save
class Editor {
    private String content;
    private int cursorPosition;

    public void type(String text) {
        content = (content == null ? "" : content) + text;
        cursorPosition = content.length();
    }

    // Creates a snapshot (memento)
    public EditorMemento save() {
        return new EditorMemento(content, cursorPosition);
    }

    // Restores from snapshot
    public void restore(EditorMemento memento) {
        this.content = memento.getContent();
        this.cursorPosition = memento.getCursorPosition();
    }
    
    public String getContent() { return content; }
}

// Memento: stores the snapshot (immutable)
class EditorMemento {
    private final String content;
    private final int cursorPosition;
    EditorMemento(String content, int cursor) {
        this.content = content; this.cursorPosition = cursor;
    }
    String getContent()      { return content; }
    int getCursorPosition()  { return cursorPosition; }
}

// Caretaker: manages snapshots (doesn't inspect memento content)
class UndoHistory {
    private final Deque<EditorMemento> history = new ArrayDeque<>();
    public void push(EditorMemento m) { history.push(m); }
    public EditorMemento pop()        { return history.isEmpty() ? null : history.pop(); }
}

// Usage
Editor editor = new Editor();
UndoHistory history = new UndoHistory();
editor.type("Hello");
history.push(editor.save());
editor.type(" World");
System.out.println(editor.getContent()); // Hello World
editor.restore(history.pop());
System.out.println(editor.getContent()); // Hello
Behavioral

Observer (Event Listener)

Very Common

Intent: Define one-to-many dependency; when subject changes, all observers notified automatically.

  • Use when: event systems, pub/sub, MVC (model notifies views), reactive systems
  • Real-world: Java EventListener, RxJava, Spring ApplicationEvent, Kafka consumers
// Observer interface
interface MetadataChangeListener {
    void onMetadataChanged(MetadataEvent event);
}

// Subject (Observable)
class DataCatalogAsset {
    private String name;
    private String description;
    private final List<MetadataChangeListener> listeners = new ArrayList<>();
    
    public void subscribe(MetadataChangeListener listener) { listeners.add(listener); }
    public void unsubscribe(MetadataChangeListener listener) { listeners.remove(listener); }
    
    private void notifyListeners(MetadataEvent event) {
        listeners.forEach(l -> l.onMetadataChanged(event));
    }
    
    public void updateDescription(String desc) {
        String old = this.description;
        this.description = desc;
        notifyListeners(new MetadataEvent("DESCRIPTION_CHANGED", old, desc, this.name));
    }
}

// Concrete observers
class AuditLogger implements MetadataChangeListener {
    public void onMetadataChanged(MetadataEvent event) {
        System.out.println("AUDIT: " + event.getAsset() + " changed: " + event.getType());
    }
}

class SearchIndexer implements MetadataChangeListener {
    public void onMetadataChanged(MetadataEvent event) {
        System.out.println("INDEX: Re-indexing " + event.getAsset() + " in Elasticsearch");
    }
}

// Usage: loosely coupled
DataCatalogAsset asset = new DataCatalogAsset();
asset.subscribe(new AuditLogger());
asset.subscribe(new SearchIndexer());
asset.updateDescription("Revenue data table"); // Both observers notified
Behavioral

State

Common

Intent: Allow object to alter behavior when internal state changes. Looks like class changed.

  • Use when: object with many states with complex transitions; vending machine, TCP, orders, traffic light
  • Vs Strategy: State transitions itself; Strategy is set from outside
interface OrderState {
    void confirm(Order order);
    void ship(Order order);
    void deliver(Order order);
    void cancel(Order order);
}

class Order {
    private OrderState state = new PendingState(); // initial state
    private String id;

    public void setState(OrderState state) { this.state = state; }
    
    public void confirm()  { state.confirm(this); }
    public void ship()     { state.ship(this); }
    public void deliver()  { state.deliver(this); }
    public void cancel()   { state.cancel(this); }
}

class PendingState implements OrderState {
    public void confirm(Order o) {
        System.out.println("Order confirmed");
        o.setState(new ConfirmedState());
    }
    public void ship(Order o)    { System.out.println("Cannot ship: not confirmed"); }
    public void deliver(Order o) { System.out.println("Cannot deliver: not shipped"); }
    public void cancel(Order o)  { System.out.println("Order cancelled"); o.setState(new CancelledState()); }
}

class ConfirmedState implements OrderState {
    public void confirm(Order o) { System.out.println("Already confirmed"); }
    public void ship(Order o)    { System.out.println("Order shipped"); o.setState(new ShippedState()); }
    public void deliver(Order o) { System.out.println("Not shipped yet"); }
    public void cancel(Order o)  { System.out.println("Order cancelled"); o.setState(new CancelledState()); }
}

class ShippedState implements OrderState {
    public void confirm(Order o) { System.out.println("Already shipped"); }
    public void ship(Order o)    { System.out.println("Already shipped"); }
    public void deliver(Order o) { System.out.println("Delivered!"); o.setState(new DeliveredState()); }
    public void cancel(Order o)  { System.out.println("Cannot cancel: already shipped"); }
}

class DeliveredState implements OrderState {
    public void confirm(Order o) { System.out.println("Order complete"); }
    public void ship(Order o)    { System.out.println("Order complete"); }
    public void deliver(Order o) { System.out.println("Already delivered"); }
    public void cancel(Order o)  { System.out.println("Cannot cancel: delivered"); }
}

class CancelledState implements OrderState {
    public void confirm(Order o) { System.out.println("Order cancelled, cannot proceed"); }
    public void ship(Order o)    { System.out.println("Order cancelled"); }
    public void deliver(Order o) { System.out.println("Order cancelled"); }
    public void cancel(Order o)  { System.out.println("Already cancelled"); }
}
Behavioral

Strategy

Very Common

Intent: Define family of algorithms, encapsulate each, make them interchangeable at runtime.

  • Use when: need to switch algorithm/behavior at runtime; replace if/else chains
  • Real-world: Java Comparator, Spring Security AuthenticationStrategy, payment processors
// Strategy interface
interface SortStrategy {
    void sort(int[] data);
}

// Concrete strategies
class QuickSortStrategy implements SortStrategy {
    public void sort(int[] data) { System.out.println("QuickSort O(n log n)"); Arrays.sort(data); }
}
class BubbleSortStrategy implements SortStrategy {
    public void sort(int[] data) {
        System.out.println("BubbleSort O(n^2)");
        // bubble sort implementation...
    }
}
class MergeSortStrategy implements SortStrategy {
    public void sort(int[] data) { System.out.println("MergeSort O(n log n) stable"); }
}

// Context: uses strategy
class DataProcessor {
    private SortStrategy strategy;
    
    public DataProcessor(SortStrategy strategy) { this.strategy = strategy; }
    
    // Can swap strategy at runtime!
    public void setStrategy(SortStrategy strategy) { this.strategy = strategy; }
    
    public void process(int[] data) {
        System.out.println("Processing " + data.length + " elements");
        strategy.sort(data);
    }
}

// Usage: choose strategy based on data size
DataProcessor processor = new DataProcessor(new QuickSortStrategy());
if (data.length < 10) processor.setStrategy(new BubbleSortStrategy());
processor.process(data);
Behavioral

Template Method

Common

Intent: Define skeleton of an algorithm in base class; let subclasses fill in steps.

  • Use when: multiple classes share same algorithm skeleton but differ in specific steps
  • Vs Strategy: Template Method uses inheritance; Strategy uses composition
  • Real-world: Java AbstractList, Spring JdbcTemplate, HttpServlet.service()
// Abstract class defines template
abstract class DataImporter {
    // Template method: defines the algorithm skeleton
    public final void importData(String source) {
        connect(source);           // step 1
        List<String> raw = extract();  // step 2 (hook - subclass implements)
        List<Object> data = transform(raw); // step 3 (hook)
        load(data);                // step 4 (hook)
        disconnect();              // step 5
        System.out.println("Import complete: " + data.size() + " records");
    }
    
    protected abstract void connect(String source);
    protected abstract List<String> extract();
    protected abstract List<Object> transform(List<String> raw);
    
    protected void load(List<Object> data) {
        System.out.println("Loading " + data.size() + " records to DB");
    }
    protected void disconnect() { System.out.println("Disconnecting"); }
}

// Subclass: only overrides variable parts
class CSVImporter extends DataImporter {
    protected void connect(String src) { System.out.println("Opening CSV: " + src); }
    protected List<String> extract()   { return List.of("row1", "row2"); }
    protected List<Object> transform(List<String> raw) {
        return raw.stream().map(r -> (Object)r.split(",")).toList();
    }
}

class JSONImporter extends DataImporter {
    protected void connect(String src) { System.out.println("Connecting to JSON API: " + src); }
    protected List<String> extract()   { return List.of("{}", "{}"); }
    protected List<Object> transform(List<String> raw) {
        return raw.stream().map(r -> (Object)parseJson(r)).toList();
    }
    private Object parseJson(String s) { return Map.of(); }
}

// Usage
new CSVImporter().importData("users.csv");
new JSONImporter().importData("https://api.alation.com/assets");
Behavioral

Visitor

Intent: Add new operations to existing class hierarchy without modifying the classes.

  • Use when: need to add many unrelated operations to stable class hierarchy; compilers (AST traversal)
  • Double dispatch: runtime dispatch based on BOTH visitor type AND element type
  • Real-world: Java compiler, XML/JSON parsers, code analysis tools
// Element hierarchy
interface MetadataElement {
    void accept(MetadataVisitor visitor);
}

class TableMetadata implements MetadataElement {
    String name, schema;
    int rowCount;
    TableMetadata(String name, String schema, int rowCount) {
        this.name = name; this.schema = schema; this.rowCount = rowCount;
    }
    public void accept(MetadataVisitor visitor) { visitor.visitTable(this); }
}

class ColumnMetadata implements MetadataElement {
    String name, type;
    boolean nullable;
    ColumnMetadata(String name, String type, boolean nullable) {
        this.name = name; this.type = type; this.nullable = nullable;
    }
    public void accept(MetadataVisitor visitor) { visitor.visitColumn(this); }
}

// Visitor interface
interface MetadataVisitor {
    void visitTable(TableMetadata table);
    void visitColumn(ColumnMetadata column);
}

// Add new operations WITHOUT modifying TableMetadata/ColumnMetadata
class DataQualityScorer implements MetadataVisitor {
    public void visitTable(TableMetadata t) {
        System.out.println("Quality score for table " + t.name + ": " + (t.rowCount > 0 ? "GOOD" : "EMPTY"));
    }
    public void visitColumn(ColumnMetadata c) {
        System.out.println("Column " + c.name + " nullable=" + c.nullable + " risk: " + (c.nullable ? "HIGH" : "LOW"));
    }
}

class DocumentationChecker implements MetadataVisitor {
    public void visitTable(TableMetadata t) {
        System.out.println("Table " + t.name + " needs documentation: " + (t.schema.isEmpty() ? "YES" : "NO"));
    }
    public void visitColumn(ColumnMetadata c) {
        System.out.println("Column " + c.name + " type=" + c.type);
    }
}

// Usage: iterate elements and apply any visitor
List<MetadataElement> elements = List.of(
    new TableMetadata("orders", "public", 1000),
    new ColumnMetadata("order_id", "BIGINT", false),
    new ColumnMetadata("notes", "TEXT", true)
);

MetadataVisitor qualityChecker = new DataQualityScorer();
elements.forEach(e -> e.accept(qualityChecker));

MetadataVisitor docChecker = new DocumentationChecker();
elements.forEach(e -> e.accept(docChecker));
LLD Design Examples

Design a Parking Lot System

LLD Classic

Classes needed:

  • ParkingLot (Singleton), ParkingFloor, ParkingSlot, Vehicle, Ticket, PaymentService
  • Slot types: Handicapped, Compact, Large, Motorcycle
  • Vehicle types: Car, Truck, Motorcycle, Van
enum SlotType { MOTORCYCLE, COMPACT, LARGE, HANDICAPPED }
enum VehicleType { MOTORCYCLE, CAR, TRUCK, VAN }

class Vehicle {
    protected String licensePlate;
    protected VehicleType type;
    Vehicle(String plate, VehicleType type) { this.licensePlate = plate; this.type = type; }
}

class ParkingSlot {
    private final String id;
    private final SlotType type;
    private Vehicle parkedVehicle;
    
    public boolean isAvailable() { return parkedVehicle == null; }
    public boolean canFit(Vehicle v) {
        return isAvailable() && switch(v.type) {
            case MOTORCYCLE -> type == SlotType.MOTORCYCLE || type == SlotType.COMPACT;
            case CAR        -> type == SlotType.COMPACT || type == SlotType.LARGE;
            case TRUCK, VAN -> type == SlotType.LARGE;
        };
    }
    public void park(Vehicle v)  { this.parkedVehicle = v; }
    public void vacate()         { this.parkedVehicle = null; }
}

class ParkingFloor {
    private final int level;
    private final List<ParkingSlot> slots;
    
    public Optional<ParkingSlot> findAvailableSlot(Vehicle vehicle) {
        return slots.stream().filter(s -> s.canFit(vehicle)).findFirst();
    }
}

class ParkingLot {
    private static ParkingLot instance;
    private final List<ParkingFloor> floors;
    private final Map<String, Ticket> activeTickets = new HashMap<>();
    
    private ParkingLot(List<ParkingFloor> floors) { this.floors = floors; }
    
    public static synchronized ParkingLot getInstance(List<ParkingFloor> floors) {
        if (instance == null) instance = new ParkingLot(floors);
        return instance;
    }
    
    public Ticket park(Vehicle vehicle) {
        for (ParkingFloor floor : floors) {
            Optional<ParkingSlot> slot = floor.findAvailableSlot(vehicle);
            if (slot.isPresent()) {
                slot.get().park(vehicle);
                Ticket ticket = new Ticket(vehicle, slot.get(), LocalDateTime.now());
                activeTickets.put(ticket.getId(), ticket);
                return ticket;
            }
        }
        throw new RuntimeException("Parking lot is full");
    }
    
    public double exit(String ticketId) {
        Ticket ticket = activeTickets.remove(ticketId);
        ticket.getSlot().vacate();
        return calculateFee(ticket);
    }
    
    private double calculateFee(Ticket ticket) {
        long hours = ChronoUnit.HOURS.between(ticket.getEntryTime(), LocalDateTime.now());
        return Math.max(1, hours) * getRatePerHour(ticket.getVehicle().type);
    }
}

Design an Elevator System

LLD Classic

Key Components & Decisions:

  • ElevatorController: manages all elevators, dispatches requests (Strategy pattern for scheduling)
  • Elevator: state machine (IDLE, MOVING_UP, MOVING_DOWN, MAINTENANCE)
  • Scheduling Algorithms: FCFS, SCAN (elevator algorithm), LOOK
  • SCAN/LOOK: elevator continues in one direction, picks up requests along the way (like a disk seek algorithm)
enum Direction { UP, DOWN, IDLE }
enum ElevatorStatus { IDLE, MOVING, DOOR_OPEN, MAINTENANCE }

class Elevator {
    private int currentFloor;
    private Direction direction;
    private ElevatorStatus status;
    private final TreeSet<Integer> upRequests   = new TreeSet<>();
    private final TreeSet<Integer> downRequests = new TreeSet<>(Comparator.reverseOrder());
    
    public void addRequest(int floor) {
        if (floor > currentFloor) upRequests.add(floor);
        else downRequests.add(floor);
    }
    
    // SCAN algorithm: serve all floors in current direction, then reverse
    public void step() {
        if (direction == Direction.UP && !upRequests.isEmpty()) {
            currentFloor = upRequests.first(); upRequests.remove(currentFloor);
            System.out.println("Elevator at floor " + currentFloor);
        } else if (!downRequests.isEmpty()) {
            direction = Direction.DOWN;
            currentFloor = downRequests.first(); downRequests.remove(currentFloor);
        } else if (!upRequests.isEmpty()) {
            direction = Direction.UP;
        } else {
            direction = Direction.IDLE; status = ElevatorStatus.IDLE;
        }
    }
}

// Controller dispatches to nearest available elevator
class ElevatorController {
    private final List<Elevator> elevators;
    
    public void requestElevator(int floor, Direction direction) {
        // Find nearest idle elevator, or least-loaded
        Elevator best = elevators.stream()
            .min(Comparator.comparingInt(e -> Math.abs(e.getCurrentFloor() - floor)))
            .orElseThrow();
        best.addRequest(floor);
    }
}

Design a Library Management System

LLD

Key Entities:

  • Book (ISBN, title, copies), BookItem (physical copy, barcode), Member, Loan, Fine, Catalog
  • Patterns: Strategy (search), Observer (overdue notifications), Singleton (library), Builder (search criteria)
class Book {
    private final String isbn;
    private final String title;
    private final List<String> authors;
    private final List<BookItem> copies = new ArrayList<>();
    
    public Optional<BookItem> getAvailableCopy() {
        return copies.stream().filter(BookItem::isAvailable).findFirst();
    }
}

class BookItem {
    private final String barcode;
    private BookStatus status; // AVAILABLE, LOANED, RESERVED, LOST
    private Member loanedTo;
    private LocalDate dueDate;
    
    public boolean isAvailable() { return status == BookStatus.AVAILABLE; }
}

class Loan {
    private final BookItem bookItem;
    private final Member member;
    private final LocalDate issueDate;
    private final LocalDate dueDate;
    private LocalDate returnDate;
    
    public double calculateFine() {
        if (returnDate == null || !returnDate.isAfter(dueDate)) return 0.0;
        long overdueDays = ChronoUnit.DAYS.between(dueDate, returnDate);
        return overdueDays * 0.50; // $0.50/day fine
    }
}

class Library {
    // Strategy pattern for search
    interface SearchStrategy {
        List<Book> search(String query, List<Book> catalog);
    }
    
    class TitleSearch implements SearchStrategy {
        public List<Book> search(String q, List<Book> catalog) {
            return catalog.stream().filter(b -> b.getTitle().toLowerCase().contains(q.toLowerCase())).toList();
        }
    }
    
    public Loan issueBook(String isbn, Member member) {
        Book book = findByISBN(isbn).orElseThrow();
        BookItem copy = book.getAvailableCopy().orElseThrow(() -> new RuntimeException("No copies available"));
        copy.setStatus(BookStatus.LOANED);
        return new Loan(copy, member, LocalDate.now(), LocalDate.now().plusDays(14));
    }
}
Java 8 — Functional Interfaces, Lambda & Method References

Core Functional Interfaces — Function, BiFunction, Predicate, Consumer, Supplier

Java 8Must Know
InterfaceSignatureMethodUse Case
Function<T,R>T Rapply(T)Transform a value
BiFunction<T,U,R>(T,U) Rapply(T,U)Combine two values
Predicate<T>T booleantest(T)Filter / condition
BiPredicate<T,U>(T,U) booleantest(T,U)Two-arg condition
Consumer<T>T voidaccept(T)Side-effect action
BiConsumer<T,U>(T,U) voidaccept(T,U)Map.forEach
Supplier<T>() Tget()Lazy factory, memoize
UnaryOperator<T>T Tapply(T)Modify in-place type
BinaryOperator<T>(T,T) Tapply(T,T)Reduce two to one
import java.util.function.*;

// ── Function ──────────────────────────────────────────
Function<String, Integer> strLen = String::length;
Function<String, String>  upper  = String::toUpperCase;

// compose: upper.compose(strLen) — wrong types; use andThen for chaining
Function<String, String> trimThenUpper = ((Function<String,String> String::trim).andThen(String::toUpperCase);
System.out.println(trimThenUpper.apply("  hello  ")); // "HELLO"

// ── BiFunction ────────────────────────────────────
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(repeat.apply("ab", 3)); // "ababab"

// andThen on BiFunction (applies another Function to the result)
BiFunction<Integer, Integer, Integer> add    = Integer::sum;
Function<Integer, String>             format = n -> "Result: " + n;
BiFunction<Integer, Integer, String>  addFmt = add.andThen(format);
System.out.println(addFmt.apply(3, 4)); // "Result: 7"

// ── Predicate ───────────────────────────────────────────
Predicate<String> nonEmpty = s -> !s.isEmpty();
Predicate<String> hasDigit  = s -> s.chars().anyMatch(Character::isDigit);

// Compose predicates
Predicate<String> validPassword = nonEmpty.and(hasDigit);
Predicate<String> reject        = nonEmpty.negate();
Predicate<String> either        = nonEmpty.or(hasDigit);

// ── Consumer ─────────────────────────────────────────────
Consumer<String> print  = System.out::println;
Consumer<String> log    = s -> System.err.println("[LOG] " + s);
Consumer<String> both   = print.andThen(log);  // chain side effects

// BiConsumer — used in Map.forEach
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87);
BiConsumer<String, Integer> printScore = (k, v) -> System.out.println(k + ": " + v);
scores.forEach(printScore);

// ── Supplier ─────────────────────────────────────────────
Supplier<List<String>> listFactory = ArrayList::new;
List<String> list = listFactory.get();

// Lazy initialization pattern
Supplier<ExpensiveObject> lazy = () -> new ExpensiveObject();
// Object not created until lazy.get() is called

Method References — 4 Types with Examples

Java 8Lambda
TypeSyntaxEquivalent Lambda
Static methodClassName::staticMethodx -> ClassName.staticMethod(x)
Instance method (specific object)instance::methodx -> instance.method(x)
Instance method (arbitrary object)ClassName::instanceMethodx -> x.instanceMethod()
ConstructorClassName::new(args) -> new ClassName(args)
// 1. Static method reference
Function<String, Integer>  parseInt  = Integer::parseInt;   // Integer.parseInt(s)
Function<Double, Double>   abs       = Math::abs;
BiFunction<Integer,Integer,Integer> max = Integer::max;

// 2. Instance method of specific object
String prefix = "Hello";
Predicate<String> startsWith = prefix::startsWith;   // s -> prefix.startsWith(s) — wait, wrong
Supplier<Integer> len        = prefix::length;        // () -> prefix.length()

// 3. Instance method of arbitrary object (most common in streams)
Function<String, String>    toUpper   = String::toUpperCase;  // s -> s.toUpperCase()
Function<String, Integer>   strLength = String::length;
Predicate<String>           isEmpty   = String::isEmpty;
Comparator<String>          cmp       = String::compareTo;

// 4. Constructor reference
Supplier<ArrayList<String>> newList   = ArrayList::new;
Function<Integer, int[]>    newArray  = int[]::new;

// Practical example — sorting
List<String> names = List.of("Charlie", "Alice", "Bob");
names.stream()
     .sorted(String::compareTo)         // method ref for Comparator
     .forEach(System.out::println);     // method ref for Consumer
Java 8 — Streams API

Stream Pipeline — Intermediate & Terminal Operations

Java 8Must Know

Stream Pipeline

Source zero or more Intermediate ops (lazy) one Terminal op (triggers execution)

Streams are not reusable — consuming a stream closes it. Intermediate ops return a new Stream; they don't process data until a terminal op is called.

import java.util.*;
import java.util.stream.*;
import java.util.function.*;

List<String> names = List.of("Alice", "Bob", "Charlie", "Adam", "Betty");

// ── Intermediate operations (lazy, return Stream) ──────────
// filter — keep elements matching predicate
Stream<String> aNames = names.stream().filter(n -> n.startsWith("A"));

// map — transform T → R
Stream<Integer> lengths = names.stream().map(String::length);

// flatMap — T → Stream<R> (flatten nested structure)
List<List<Integer>> nested = List.of(List.of(1,2), List.of(3,4));
List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());  // [1, 2, 3, 4]

// distinct, sorted, limit, skip, peek
names.stream()
     .filter(n -> n.length() > 3)
     .distinct()
     .sorted(Comparator.reverseOrder())
     .limit(3)
     .peek(n -> System.out.println("Processing: " + n))  // debug, not for side effects
     .collect(Collectors.toList());

// ── Terminal operations (eager, trigger pipeline) ──────────
// collect
List<String> upper = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

// reduce — fold into single value
int totalLen = names.stream()
    .mapToInt(String::length)
    .sum();                             // shorthand; also: .reduce(0, Integer::sum)

Optional<String> longest = names.stream()
    .reduce((a, b) -> a.length() >= b.length() ? a : b);

// count, min, max, findFirst, findAny, anyMatch, allMatch, noneMatch
long count   = names.stream().filter(n -> n.contains("a")).count();
boolean any  = names.stream().anyMatch(n -> n.startsWith("Z"));
Optional<String> first = names.stream().filter(n -> n.length() == 3).findFirst();

Collectors — groupingBy, partitioningBy, joining, toMap

Java 8Streams
import java.util.stream.Collectors;

record Employee(String name, String dept, int salary) {}
List<Employee> employees = List.of(
    new Employee("Alice", "Eng", 95000),
    new Employee("Bob",   "Eng", 88000),
    new Employee("Carol", "HR",  72000),
    new Employee("Dan",   "HR",  68000)
);

// ── groupingBy ─────────────────────────────────────────────
Map<String, List<Employee>> byDept =
    employees.stream().collect(Collectors.groupingBy(Employee::dept));

// With downstream collector — count per dept
Map<String, Long> countByDept =
    employees.stream().collect(Collectors.groupingBy(Employee::dept, Collectors.counting()));

// Average salary per dept
Map<String, Double> avgSalary =
    employees.stream().collect(Collectors.groupingBy(Employee::dept,
        Collectors.averagingInt(Employee::salary)));

// Nested grouping — dept → salary bucket → names
Map<String, Map<String, List<String>>> nested =
    employees.stream().collect(Collectors.groupingBy(Employee::dept,
        Collectors.groupingBy(
            e -> e.salary() >= 80000 ? "senior" : "junior",
            Collectors.mapping(Employee::name, Collectors.toList()))));

// ── partitioningBy ─────────────────────────────────────────
Map<Boolean, List<Employee>> seniorJunior =
    employees.stream().collect(Collectors.partitioningBy(e -> e.salary() >= 80000));

// ── joining ────────────────────────────────────────────────
String csv    = employees.stream().map(Employee::name).collect(Collectors.joining(", "));
String report = employees.stream().map(Employee::name)
    .collect(Collectors.joining(", ", "[", "]")); // "[Alice, Bob, Carol, Dan]"

// ── toMap ──────────────────────────────────────────────────
Map<String, Integer> nameSalary =
    employees.stream().collect(Collectors.toMap(Employee::name, Employee::salary));

// Merge function for duplicate keys
Map<String, Integer> deptMaxSalary =
    employees.stream().collect(Collectors.toMap(
        Employee::dept,
        Employee::salary,
        Integer::max));   // merge: keep higher salary on duplicate dept key

// ── summarizingInt ─────────────────────────────────────────
IntSummaryStatistics stats =
    employees.stream().collect(Collectors.summarizingInt(Employee::salary));
System.out.println(stats.getMax() + " / " + stats.getAverage());

Optional, Comparator Chains & Parallel Streams

Java 8Optional
// Optional — avoid NullPointerException, express optionality in type
Optional<String> opt = Optional.of("hello");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(maybeNull);

// Extract value
String val = opt.get();                          // throws if empty — avoid
String val2 = opt.orElse("default");            // safe default
String val3 = opt.orElseGet(() -> compute());   // lazy default
String val4 = opt.orElseThrow(IllegalArgumentException::new);

// Transform
Optional<Integer> len = opt.map(String::length);           // Optional<Integer>
Optional<String> upper = opt.map(String::toUpperCase);
Optional<String> nested = opt.flatMap(s -> Optional.of(s.trim())); // avoid Optional<Optional>

// Filter
Optional<String> filtered = opt.filter(s -> s.length() > 3);

// Side effects
opt.ifPresent(System.out::println);
opt.ifPresentOrElse(System.out::println, () -> System.out.println("empty")); // Java 9

// Chaining Optional in service layer
Optional<User> user = userRepo.findById(id)
    .filter(User::isActive)
    .map(u -> { u.setLastSeen(now()); return u; });
// Comparator.comparing — declarative chaining
List<Employee> sorted = employees.stream()
    .sorted(Comparator.comparing(Employee::dept)
                      .thenComparing(Employee::salary, Comparator.reverseOrder())
                      .thenComparing(Employee::name))
    .collect(Collectors.toList());

// Null-safe
Comparator<Employee> nullSafe =
    Comparator.comparing(Employee::dept, Comparator.nullsLast(Comparator.naturalOrder()));

// naturalOrder / reverseOrder
List<Integer> nums = List.of(3,1,4,1,5,9);
nums.stream().sorted(Comparator.reverseOrder()).forEach(System.out::println);
// Parallel streams — uses ForkJoinPool.commonPool()
long count = LongStream.range(0, 1_000_000)
    .parallel()
    .filter(n -> n % 2 == 0)
    .count();

// When to use parallel:
//  CPU-bound, large datasets (>10k elements)
//  Stateless, independent operations (map, filter)
//  I/O bound (blocks threads in shared pool)
//  Operations with shared mutable state
//  Small datasets — overhead > benefit
//  Ordered operations (findFirst may be slow parallel)

// Custom ForkJoinPool (don't pollute common pool)
ForkJoinPool pool = new ForkJoinPool(4);
List<String> result = pool.submit(() ->
    names.parallelStream()
         .map(String::toUpperCase)
         .collect(Collectors.toList())
).get();

Real-World Stream Patterns — Practical Examples

Java 8Streams
// ── Pattern 1: Word frequency count ────────────────────────
String text = "the cat sat on the mat the cat";
Map<String, Long> freq = Arrays.stream(text.split("\\s+"))
    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
// Sort by frequency descending
freq.entrySet().stream()
    .sorted(Map.Entry.<String,Long>comparingByValue().reversed())
    .limit(5)
    .forEach(e -> System.out.println(e.getKey() + ": " + e.getValue()));

// ── Pattern 2: Flatten nested lists and deduplicate ────────
List<List<String>> matrix = List.of(List.of("a","b","c"), List.of("b","c","d"));
List<String> unique = matrix.stream()
    .flatMap(List::stream)
    .distinct()
    .sorted()
    .collect(Collectors.toList()); // [a, b, c, d]

// ── Pattern 3: Group and transform ────────────────────────
// Max salary employee per department
Map<String, Optional<Employee>> topEarner =
    employees.stream().collect(
        Collectors.groupingBy(Employee::dept,
            Collectors.maxBy(Comparator.comparingInt(Employee::salary))));

// ── Pattern 4: Build index map from list ───────────────────
Map<Integer, String> idxMap = IntStream.range(0, names.size())
    .boxed()
    .collect(Collectors.toMap(Function.identity(), names::get));

// ── Pattern 5: Aggregate stats ────────────────────────────
OptionalDouble avgSalaryOpt = employees.stream()
    .mapToInt(Employee::salary)
    .average();

// ── Pattern 6: Chained BiFunction for transformation pipeline
BiFunction<String, String, String> concat   = (a, b) -> a + b;
BiFunction<String, String, Integer> compareLen = (a, b) -> Integer.compare(a.length(), b.length());

// andThen: apply Function to the result of BiFunction
BiFunction<String, String, String> concatUpper = concat.andThen(String::toUpperCase);
System.out.println(concatUpper.apply("hello", " world")); // "HELLO WORLD"