OOP, SOLID & All Design Patterns
SOLID Principles All 23 GoF Patterns (Creational, Structural, Behavioral) LLD Examples
SOLID Principles
Singleton
Factory Method
Abstract Factory
Builder
Prototype
Adapter
Bridge
Composite
Decorator
Facade
Flyweight
Proxy
Chain of Resp.
Command
Iterator
Mediator
Memento
Observer
State
Strategy
Template Method
Visitor
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
| Interface | Signature | Method | Use Case |
|---|---|---|---|
Function<T,R> | T R | apply(T) | Transform a value |
BiFunction<T,U,R> | (T,U) R | apply(T,U) | Combine two values |
Predicate<T> | T boolean | test(T) | Filter / condition |
BiPredicate<T,U> | (T,U) boolean | test(T,U) | Two-arg condition |
Consumer<T> | T void | accept(T) | Side-effect action |
BiConsumer<T,U> | (T,U) void | accept(T,U) | Map.forEach |
Supplier<T> | () T | get() | Lazy factory, memoize |
UnaryOperator<T> | T T | apply(T) | Modify in-place type |
BinaryOperator<T> | (T,T) T | apply(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
| Type | Syntax | Equivalent Lambda |
|---|---|---|
| Static method | ClassName::staticMethod | x -> ClassName.staticMethod(x) |
| Instance method (specific object) | instance::method | x -> instance.method(x) |
| Instance method (arbitrary object) | ClassName::instanceMethod | x -> x.instanceMethod() |
| Constructor | ClassName::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"