This is the level that separates good Java developers from great ones. Concurrency bugs are among the hardest to find and fix. Understanding the JVM's memory model, garbage collection, and classic design patterns lets you build systems that are correct, performant, and maintainable under real-world conditions.


Step 1 — Threads: The Foundation

ThreadBasics.javajava
public class ThreadBasics {

    // Way 1: Extend Thread (less flexible — Java only allows single inheritance)
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Thread: " + getName() + " is running");
        }
    }

    // Way 2: Implement Runnable (PREFERRED — separates task from thread)
    static class MyTask implements Runnable {
        private final String name;
        MyTask(String name) { this.name = name; }

        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println(name + " - iteration " + i +
                    " on thread: " + Thread.currentThread().getName());
                try { Thread.sleep(100); } // pause 100ms, throws InterruptedException
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // restore interrupt flag!
                    return;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // Starting threads
        Thread t1 = new MyThread();
        t1.setName("Worker-1");
        t1.start(); // starts NEW thread — NEVER call run() directly!

        Thread t2 = new Thread(new MyTask("Task-A"), "Worker-2");
        t2.start();

        // Lambda shorthand for Runnable
        Thread t3 = new Thread(() -> System.out.println("Lambda thread!"), "Worker-3");
        t3.start();

        // join(): wait for a thread to finish before continuing
        t1.join(); // main thread BLOCKS until t1 completes
        t2.join();
        t3.join();
        System.out.println("All threads done");

        // Thread states: NEW -> RUNNABLE -> BLOCKED/WAITING/TIMED_WAITING -> TERMINATED
        System.out.println("t1 state: " + t1.getState()); // TERMINATED

        // Daemon threads: background threads that die when main thread dies
        Thread daemon = new Thread(() -> {
            while (true) { /* background work */ }
        });
        daemon.setDaemon(true); // must set BEFORE start()
        // daemon.start(); // dies automatically when JVM exits
    }
}

Step 2 — Thread Safety and Synchronization

When multiple threads access shared mutable state without proper synchronization, you get race conditions — bugs that only appear under concurrent load and are notoriously difficult to reproduce.

ThreadSafety.javajava
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.*;

public class ThreadSafety {

    // ============================================================
    // PROBLEM: Race Condition (unsynchronized counter)
    // ============================================================
    static int unsafeCount = 0;
    static void unsafeIncrement() { unsafeCount++; } // NOT atomic: read, add, write = 3 ops
    // If two threads both read 5, both compute 6, both write 6 → count becomes 6 not 7!

    // ============================================================
    // SOLUTION 1: synchronized keyword
    // ============================================================
    static int syncCount = 0;
    static synchronized void syncIncrement() {
        syncCount++; // only ONE thread can execute this at a time
    }
    // Also works on instance methods and blocks:
    // synchronized(this) { ... }  — locks on 'this' object
    // synchronized(SomeClass.class) { ... } — locks on class object (for static data)

    // ============================================================
    // SOLUTION 2: AtomicInteger — lock-free, hardware-level CAS operations
    // BEST for single variable counters/flags
    // ============================================================
    static AtomicInteger atomicCount = new AtomicInteger(0);
    static void atomicIncrement() {
        atomicCount.incrementAndGet(); // atomic! reads and increments in one CPU instruction
    }

    // Other useful Atomic classes: AtomicLong, AtomicBoolean, AtomicReference<T>
    static AtomicReference<String> atomicRef = new AtomicReference<>("initial");

    // ============================================================
    // SOLUTION 3: ReentrantLock — explicit lock with more control
    // Use when you need tryLock(), lockInterruptibly(), or multiple conditions
    // ============================================================
    static int lockCount = 0;
    static final ReentrantLock lock = new ReentrantLock(true); // fair=true: FIFO ordering

    static void lockIncrement() {
        lock.lock();       // acquire lock — blocks if another thread holds it
        try {
            lockCount++;
        } finally {
            lock.unlock(); // ALWAYS unlock in finally — even if exception thrown!
        }
    }

    // tryLock(): attempt to acquire without blocking
    static boolean tryIncrement() {
        if (lock.tryLock()) { // returns immediately: true=acquired, false=couldn't
            try {
                lockCount++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false; // lock was held by another thread
    }

    // ============================================================
    // SOLUTION 4: ReadWriteLock — multiple readers OR one writer
    // Perfect for read-heavy scenarios (caches, configuration)
    // ============================================================
    static final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    static String config = "default";

    static String readConfig() {
        rwLock.readLock().lock();   // multiple threads can hold read lock simultaneously
        try {
            return config;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    static void writeConfig(String newVal) {
        rwLock.writeLock().lock(); // exclusive: no other readers or writers
        try {
            config = newVal;
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // ============================================================
    // volatile: ensures visibility across threads (NOT atomicity!)
    // ============================================================
    // Without volatile, JVM may cache the value in a thread-local register
    static volatile boolean running = true;
    // safe to use volatile for simple flags read/written by one thread, read by others
}

Step 3 — ExecutorService: Thread Pool Management

Creating a new Thread for each task is expensive (creates OS thread, allocates stack memory). ExecutorService manages a pool of reusable threads, dramatically improving performance.

ExecutorServiceExample.javajava
import java.util.concurrent.*;
import java.util.*;

public class ExecutorServiceExample {
    public static void main(String[] args) throws Exception {

        // ============================================================
        // Common thread pool types
        // ============================================================

        // Fixed pool: always has N threads. Best for CPU-bound tasks.
        ExecutorService fixedPool = Executors.newFixedThreadPool(4);

        // Cached pool: creates threads on demand, reuses idle threads, 60s timeout.
        // Best for short-lived async tasks. Risk: can create too many threads under load.
        ExecutorService cachedPool = Executors.newCachedThreadPool();

        // Single thread: guarantees sequential execution.
        ExecutorService singleThread = Executors.newSingleThreadExecutor();

        // Scheduled pool: for delayed or periodic tasks.
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

        // Virtual threads (Java 21+): massive scale-out for I/O-bound tasks
        // ExecutorService virtualPool = Executors.newVirtualThreadPerTaskExecutor();

        // ============================================================
        // submit() with Callable<T>: returns Future<T>
        // Callable = Runnable but can return a result and throw checked exceptions
        // ============================================================
        ExecutorService pool = Executors.newFixedThreadPool(3);

        Callable<Integer> heavyComputation = () -> {
            Thread.sleep(500);
            return 42;
        };

        Future<Integer> future = pool.submit(heavyComputation);

        // Do other work while computation runs on another thread...
        System.out.println("Doing other work...");

        // get() BLOCKS until the result is ready
        Integer result = future.get(2, TimeUnit.SECONDS); // timeout: don't block forever!
        System.out.println("Result: " + result); // 42

        // ============================================================
        // invokeAll(): submit multiple tasks, wait for ALL to finish
        // ============================================================
        List<Callable<String>> tasks = List.of(
            () -> { Thread.sleep(100); return "Task-1 done"; },
            () -> { Thread.sleep(200); return "Task-2 done"; },
            () -> { Thread.sleep(50);  return "Task-3 done"; }
        );

        List<Future<String>> futures = pool.invokeAll(tasks);
        for (Future<String> f : futures) {
            System.out.println(f.get()); // all guaranteed to be done
        }

        // ============================================================
        // invokeAny(): submit multiple tasks, return FIRST result, cancel rest
        // ============================================================
        String fastest = pool.invokeAny(tasks);
        System.out.println("Fastest: " + fastest); // whichever finished first

        // ============================================================
        // Scheduled tasks
        // ============================================================
        // Run once after 1 second delay
        scheduler.schedule(() -> System.out.println("Delayed task!"), 1, TimeUnit.SECONDS);

        // Run every 2 seconds (fixed rate)
        ScheduledFuture<?> periodic = scheduler.scheduleAtFixedRate(
            () -> System.out.println("Heartbeat: " + System.currentTimeMillis()),
            0, 2, TimeUnit.SECONDS
        );
        Thread.sleep(5100);
        periodic.cancel(false); // stop the periodic task

        // ============================================================
        // ALWAYS shut down executor service (otherwise JVM won't exit!)
        // ============================================================
        pool.shutdown(); // stop accepting new tasks, finish existing ones
        if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
            pool.shutdownNow(); // force cancel remaining tasks
        }
        scheduler.shutdown();
    }
}

Step 4 — CompletableFuture: Async, Non-Blocking Pipelines

CompletableFutureExample.javajava
import java.util.concurrent.*;
import java.util.*;

public class CompletableFutureExample {

    // Simulated async service calls
    static CompletableFuture<String> fetchUser(int id) {
        return CompletableFuture.supplyAsync(() -> {
            simulateDelay(300);
            return "User-" + id;
        });
    }

    static CompletableFuture<String> fetchOrders(String user) {
        return CompletableFuture.supplyAsync(() -> {
            simulateDelay(200);
            return "Orders for " + user + ": [Order-1, Order-2]";
        });
    }

    static CompletableFuture<Double> fetchExchangeRate() {
        return CompletableFuture.supplyAsync(() -> {
            simulateDelay(150);
            return 83.5; // USD to INR
        });
    }

    static void simulateDelay(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    }

    public static void main(String[] args) throws Exception {

        // ============================================================
        // Basic chain: supplyAsync -> thenApply -> thenAccept
        // ============================================================
        CompletableFuture<Void> pipeline = CompletableFuture
            .supplyAsync(() -> "  Hello World  ")  // runs on ForkJoinPool.commonPool()
            .thenApply(String::trim)               // transform (like Stream.map)
            .thenApply(String::toUpperCase)
            .thenAccept(System.out::println);      // consume final result

        pipeline.get(); // HELLO WORLD

        // ============================================================
        // Sequential chain: thenCompose (flatMap equivalent)
        // When next step ITSELF returns a CompletableFuture
        // ============================================================
        CompletableFuture<String> ordersChain = fetchUser(1)
            .thenCompose(user -> fetchOrders(user)); // sequential: user first, then orders

        System.out.println(ordersChain.get());
        // Orders for User-1: [Order-1, Order-2]

        // ============================================================
        // Parallel: thenCombine — wait for TWO independent futures
        // ============================================================
        CompletableFuture<String> userFuture  = fetchUser(1);
        CompletableFuture<Double> rateFuture  = fetchExchangeRate();

        // Both run in PARALLEL. Combine when BOTH complete.
        CompletableFuture<String> combined = userFuture.thenCombine(
            rateFuture,
            (user, rate) -> user + " (rate: " + rate + ")"
        );
        System.out.println(combined.get()); // User-1 (rate: 83.5)

        // ============================================================
        // allOf: wait for ALL CompletableFutures
        // ============================================================
        CompletableFuture<String> f1 = fetchUser(1);
        CompletableFuture<String> f2 = fetchUser(2);
        CompletableFuture<String> f3 = fetchUser(3);

        long start = System.currentTimeMillis();
        CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
        all.get(); // waits until ALL three complete
        System.out.println("All done in: " + (System.currentTimeMillis() - start) + "ms");
        // ~300ms (parallel) vs ~900ms (sequential)

        // Collect results after allOf
        List<String> users = List.of(f1.join(), f2.join(), f3.join());
        System.out.println(users);

        // ============================================================
        // anyOf: return whichever completes FIRST
        // ============================================================
        CompletableFuture<Object> fastest = CompletableFuture.anyOf(
            fetchUser(10), fetchExchangeRate(), fetchUser(20)
        );
        System.out.println("Fastest result: " + fastest.get());

        // ============================================================
        // Error handling
        // ============================================================
        CompletableFuture<String> withError = CompletableFuture
            .supplyAsync(() -> {
                if (true) throw new RuntimeException("Service unavailable");
                return "never";
            })
            .exceptionally(ex -> {
                System.out.println("Error caught: " + ex.getMessage());
                return "fallback value"; // provide default on failure
            })
            .handle((result, ex) -> { // handle: receives result OR exception
                if (ex != null) return "Error: " + ex.getMessage();
                return result.toUpperCase();
            });

        System.out.println(withError.get());

        // ============================================================
        // Use custom executor (don't exhaust ForkJoinPool for I/O tasks)
        // ============================================================
        ExecutorService ioPool = Executors.newFixedThreadPool(10);
        CompletableFuture<String> withCustomPool = CompletableFuture
            .supplyAsync(() -> "result from I/O", ioPool) // 2nd arg: executor
            .thenApplyAsync(String::toUpperCase, ioPool);
        System.out.println(withCustomPool.get());
        ioPool.shutdown();
    }
}

Step 5 — JVM Internals: Memory Areas and Garbage Collection

Understanding JVM memory areas lets you diagnose OutOfMemoryErrors, tune performance, and understand why certain code is slow.

JVM Memory Areas

  • Heap: Where ALL objects are allocated. Divided into Young Generation (Eden + Survivor spaces) and Old Generation (Tenured). Managed by Garbage Collector. Shared across all threads.
  • Stack: One per thread. Stores local variables, method call frames, and references to heap objects. Fixed size — StackOverflowError if exceeded (infinite recursion).
  • Metaspace (Java 8+): Stores class metadata, method bytecode, static fields. Grows dynamically (no PermGen OOM). Configurable with -XX:MaxMetaspaceSize.
  • Code Cache: Stores JIT-compiled native code. The JIT compiler watches hot methods and compiles them to native for speed.
  • PC Register: Program Counter per thread — tracks which bytecode instruction the thread is currently executing.

Garbage Collection Algorithms

  • Serial GC (-XX:+UseSerialGC): Single-threaded GC. Stop-the-world pauses. Only for single-core or tiny apps.
  • Parallel GC (-XX:+UseParallelGC): Multi-threaded throughput GC. Good for batch processing. Still has stop-the-world pauses.
  • G1 GC (-XX:+UseG1GC): Default since Java 9. Divides heap into regions. Concurrent + parallel. Targets predictable pause times. Best for most applications.
  • ZGC (-XX:+UseZGC): Ultra-low latency. Sub-millisecond pauses regardless of heap size. Java 15+ production ready. Best for latency-critical apps.
  • Shenandoah (-XX:+UseShenandoahGC): Similar to ZGC. Concurrent evacuation. OpenJDK only.
JVM Tuning Flagsbash
# Common JVM flags for production
java \
  -Xms512m              \ # initial heap size (set equal to Xmx to avoid resizing)
  -Xmx2g                \ # maximum heap size
  -XX:+UseG1GC          \ # use G1 garbage collector
  -XX:MaxGCPauseMillis=200 \ # target max GC pause time (G1 tries to honor this)
  -XX:+PrintGCDetails   \ # log GC events (for diagnosis)
  -XX:+HeapDumpOnOutOfMemoryError \ # dump heap on OOM for analysis
  -XX:HeapDumpPath=/tmp/heap.hprof \
  -XX:+UseStringDeduplication \ # G1: deduplicate identical Strings on heap
  -Xss512k              \ # thread stack size (reduce if creating many threads)
  -XX:MetaspaceSize=256m \ # initial metaspace size
  -server               \ # use server JIT (more aggressive optimizations)
  MyApplication

Step 6 — Concurrent Collections

ConcurrentCollections.javajava
import java.util.concurrent.*;

public class ConcurrentCollections {
    public static void main(String[] args) throws InterruptedException {

        // ============================================================
        // ConcurrentHashMap: thread-safe Map
        // Segment locking (not full-table lock) -> high concurrency
        // ============================================================
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("a", 1);
        map.put("b", 2);

        // Atomic compound operations
        map.computeIfAbsent("c", k -> k.length()); // compute only if key absent
        map.merge("a", 10, Integer::sum); // merge: a = 1 + 10 = 11
        System.out.println(map); // {a=11, b=2, c=1}

        // ============================================================
        // CopyOnWriteArrayList: thread-safe List for read-heavy workloads
        // Write operations create a fresh copy of the underlying array
        // Reads are NEVER locked — perfect for event listener lists
        // ============================================================
        CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
        cowList.add("listener1");
        cowList.add("listener2");
        // Iterate safely even while another thread is adding/removing
        for (String s : cowList) { System.out.println(s); }

        // ============================================================
        // BlockingQueue: thread-safe FIFO queue with blocking operations
        // Classic Producer-Consumer pattern
        // ============================================================
        BlockingQueue<String> queue = new LinkedBlockingQueue<>(10); // capacity 10

        // Producer thread
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    queue.put("item-" + i); // BLOCKS if queue is full
                    System.out.println("Produced: item-" + i);
                    Thread.sleep(100);
                }
                queue.put("STOP"); // poison pill to signal consumer to stop
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        // Consumer thread
        Thread consumer = new Thread(() -> {
            try {
                while (true) {
                    String item = queue.take(); // BLOCKS if queue is empty
                    if ("STOP".equals(item)) break;
                    System.out.println("Consumed: " + item);
                }
            } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        });

        producer.start();
        consumer.start();
        producer.join();
        consumer.join();

        // Other BlockingQueue implementations:
        // ArrayBlockingQueue  — bounded, array-backed, fair ordering option
        // PriorityBlockingQueue — unbounded, sorted by priority
        // SynchronousQueue    — zero capacity: put() blocks until another thread calls take()
        // DelayQueue          — elements can only be taken after their delay expires
    }
}

Step 7 — Design Patterns in Java

Design patterns are proven solutions to recurring design problems. The Gang of Four (GoF) book categorizes them into Creational, Structural, and Behavioral. Here are the most important ones with idiomatic Java implementations.

Singleton.java — Thread-safe Singletonjava
// PATTERN: Singleton (Creational)
// Ensures only ONE instance of a class exists

// Best implementation: Enum Singleton (Josh Bloch's recommendation)
public enum DatabaseConnection {
    INSTANCE; // JVM guarantees single instantiation, thread-safe, serialization-safe

    private final String url = "jdbc:postgresql://localhost/mydb";

    public void query(String sql) {
        System.out.println("Querying [" + url + "]: " + sql);
    }
}

// Alternative: Double-checked locking (if you need a class, not enum)
class ConfigManager {
    // volatile prevents instruction reordering during initialization
    private static volatile ConfigManager instance;
    private final String env;

    private ConfigManager() {
        this.env = System.getenv().getOrDefault("APP_ENV", "development");
    }

    public static ConfigManager getInstance() {
        if (instance == null) {             // first check: avoid locking every time
            synchronized (ConfigManager.class) {
                if (instance == null) {     // second check: inside synchronized block
                    instance = new ConfigManager(); // safe initialization
                }
            }
        }
        return instance;
    }

    public String getEnv() { return env; }
}

class SingletonDemo {
    public static void main(String[] args) {
        DatabaseConnection.INSTANCE.query("SELECT * FROM users");
        DatabaseConnection.INSTANCE.query("SELECT * FROM orders");
        // Same INSTANCE used both times

        System.out.println(ConfigManager.getInstance().getEnv());
    }
}
Builder.java — Builder Patternjava
// PATTERN: Builder (Creational)
// Constructs complex objects step by step. Avoids telescoping constructors.

public class HttpRequest {
    // All fields final — immutable after build
    private final String method;
    private final String url;
    private final java.util.Map<String, String> headers;
    private final String body;
    private final int timeoutMs;
    private final boolean followRedirects;

    // Private constructor — only Builder can call it
    private HttpRequest(Builder builder) {
        this.method          = builder.method;
        this.url             = builder.url;
        this.headers         = java.util.Collections.unmodifiableMap(builder.headers);
        this.body            = builder.body;
        this.timeoutMs       = builder.timeoutMs;
        this.followRedirects = builder.followRedirects;
    }

    // Getters...
    public String getMethod() { return method; }
    public String getUrl()    { return url; }

    @Override
    public String toString() {
        return method + " " + url + " (timeout: " + timeoutMs + "ms)";
    }

    // Static nested Builder class
    public static class Builder {
        // Required fields
        private final String method;
        private final String url;
        // Optional fields with defaults
        private java.util.Map<String, String> headers = new java.util.HashMap<>();
        private String body = null;
        private int timeoutMs = 5000;
        private boolean followRedirects = true;

        public Builder(String method, String url) {
            if (method == null || url == null) throw new NullPointerException();
            this.method = method;
            this.url    = url;
        }

        public Builder header(String key, String value) {
            this.headers.put(key, value);
            return this; // return this to enable method chaining
        }

        public Builder body(String body) {
            this.body = body; return this;
        }

        public Builder timeout(int ms) {
            if (ms <= 0) throw new IllegalArgumentException("Timeout must be positive");
            this.timeoutMs = ms; return this;
        }

        public Builder followRedirects(boolean follow) {
            this.followRedirects = follow; return this;
        }

        public HttpRequest build() {
            return new HttpRequest(this);
        }
    }

    public static void main(String[] args) {
        HttpRequest request = new HttpRequest.Builder("POST", "https://api.example.com/users")
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer token123")
            .body("{\"name\": \"Kuldeep\"}")
            .timeout(10000)
            .followRedirects(false)
            .build();

        System.out.println(request);
        // POST https://api.example.com/users (timeout: 10000ms)
    }
}
Observer.java — Observer Patternjava
// PATTERN: Observer (Behavioral)
// Defines a one-to-many dependency: when one object changes state,
// all dependents are notified automatically.

import java.util.*;

// Subject (Observable)
class EventBus {
    private final Map<String, List<EventListener>> listeners = new HashMap<>();

    public void subscribe(String eventType, EventListener listener) {
        listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener);
    }

    public void unsubscribe(String eventType, EventListener listener) {
        listeners.getOrDefault(eventType, List.of()).remove(listener);
    }

    public void publish(String eventType, Object data) {
        List<EventListener> subs = listeners.getOrDefault(eventType, List.of());
        subs.forEach(listener -> listener.onEvent(eventType, data));
    }
}

// Observer interface
@FunctionalInterface
interface EventListener {
    void onEvent(String eventType, Object data);
}

// Concrete observers
class EmailService {
    public void sendWelcomeEmail(String eventType, Object data) {
        System.out.println("[Email] Sending welcome email to: " + data);
    }
}

class AuditLogger {
    public void log(String eventType, Object data) {
        System.out.println("[Audit] Event: " + eventType + ", Data: " + data);
    }
}

class ObserverDemo {
    public static void main(String[] args) {
        EventBus bus = new EventBus();
        EmailService emailService = new EmailService();
        AuditLogger   auditLogger = new AuditLogger();

        // Subscribe — lambda works because EventListener is @FunctionalInterface
        bus.subscribe("USER_REGISTERED", emailService::sendWelcomeEmail);
        bus.subscribe("USER_REGISTERED", auditLogger::log);
        bus.subscribe("ORDER_PLACED",    auditLogger::log);
        bus.subscribe("ORDER_PLACED",    (type, data) ->
            System.out.println("[Analytics] Tracking order: " + data));

        // Publish events
        bus.publish("USER_REGISTERED", "kuldeep@example.com");
        // [Email] Sending welcome email to: kuldeep@example.com
        // [Audit] Event: USER_REGISTERED, Data: kuldeep@example.com

        bus.publish("ORDER_PLACED", "ORDER-12345");
        // [Audit] Event: ORDER_PLACED, Data: ORDER-12345
        // [Analytics] Tracking order: ORDER-12345
    }
}
Strategy.java — Strategy Patternjava
// PATTERN: Strategy (Behavioral)
// Defines a family of algorithms, encapsulates each one,
// and makes them interchangeable at runtime.

import java.util.*;

// Strategy interface
@FunctionalInterface
interface SortStrategy {
    void sort(int[] arr);
}

// Context: uses a strategy
class Sorter {
    private SortStrategy strategy;

    public Sorter(SortStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(SortStrategy strategy) { // swap strategy at runtime!
        this.strategy = strategy;
    }

    public void sort(int[] arr) {
        strategy.sort(arr);
    }
}

// Concrete strategies
class BubbleSort implements SortStrategy {
    @Override
    public void sort(int[] arr) {
        System.out.println("Using Bubble Sort...");
        // O(n²) — for demonstration
        for (int i = 0; i < arr.length - 1; i++)
            for (int j = 0; j < arr.length - i - 1; j++)
                if (arr[j] > arr[j+1]) { int t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t; }
    }
}

class QuickSort implements SortStrategy {
    @Override
    public void sort(int[] arr) {
        System.out.println("Using Quick Sort...");
        Arrays.sort(arr); // simplified: Java's Arrays.sort uses Dual-Pivot Quicksort
    }
}

class StrategyDemo {
    public static void main(String[] args) {
        int[] data = {5, 3, 8, 1, 9, 2};

        Sorter sorter = new Sorter(new BubbleSort());
        sorter.sort(data);
        System.out.println(Arrays.toString(data));

        // Swap strategy based on data size at runtime
        if (data.length > 1000) {
            sorter.setStrategy(new QuickSort());
        }
        // Even cleaner with lambdas:
        sorter.setStrategy(arr -> Arrays.sort(arr)); // strategy is just a lambda!
    }
}

Other Essential Design Patterns

  • Factory Method: Define an interface for creating objects, but let subclasses decide which class to instantiate. Used in java.util.Calendar.getInstance().
  • Abstract Factory: Create families of related objects. Example: javax.xml.parsers.DocumentBuilderFactory.
  • Decorator: Add behavior to objects dynamically. Java I/O streams are the canonical example: new BufferedReader(new FileReader(file)).
  • Proxy: Control access to an object. Java's dynamic proxies (java.lang.reflect.Proxy) power Spring AOP and Hibernate lazy loading.
  • Command: Encapsulate a request as an object. Enables undo/redo, queuing, and logging of operations.
  • Template Method: Define skeleton of algorithm in base class, let subclasses fill in the steps. AbstractList in Java Collections is a great example.