Intermediate → Advanced
0%
Lesson 1·Foundations

The OOP Mental Model

Before writing code, you need to think like an OOP engineer. This lesson builds the mental framework that everything else plugs into — and corrects the most common beginner misconceptions.

Most beginners treat OOP as just another syntax. That's the wrong frame. OOP is a way of modeling reality in code. The question shifts from "what does my program do?" to "what things exist in my program, and how do they communicate?"

The Shift from Procedural Thinking

In procedural code, you write functions that operate on external data. In OOP, data and behavior are bundled into objects. Objects own their state and expose behaviors — nothing outside can reach in and corrupt their internals.

ProceduralObject-Oriented
calculateArea(shape)shape.calculateArea()
Global/shared data passed aroundObjects own their data exclusively
Logic lives in standalone functionsLogic lives in methods on responsible classes
Hard to scale past ~1000 linesScales to millions of lines (Android, Spring, etc.)
Stack vs Heap — Know This Cold

Primitives (int, double, boolean) live on the stack — fast, auto-managed, copied on method calls. Objects live on the heap — garbage collected. When you pass an object to a method, Java copies the reference, not the object. This means the method can mutate your object's state. This distinction causes subtle, hard-to-find bugs.

Main.javaJava 17+
// Primitives are COPIED into methods — original is unchanged
int score = 90;
doubleIt(score);
System.out.println(score); // still 90 — the copy was doubled, not this

// Objects: the REFERENCE is copied — same heap object is mutated
int[] arr = {1, 2, 3};
zeroFirst(arr);
System.out.println(arr[0]); // 0 — same array on the heap was modified

// null = reference points to nothing. Always guard against it.
String s = null;
// s.length(); // NullPointerException at runtime — classic beginner crash
if (s != null) { process(s); } // defensive null-check
Encapsulation

Guard your data

Private fields, controlled access. No external code can corrupt your object's state.

Inheritance

Reuse without copy-paste

Child classes inherit parent behavior and can extend or override it. Write once, reuse everywhere.

Polymorphism

One interface, many forms

Same method call, different behavior based on actual runtime type. Enables plug-and-play architecture.

Abstraction

Hide complexity

Expose only what callers need. They don't need to understand internals to use your class correctly.

Knowledge Check
You pass an int[] array to a method and modify arr[0] inside. Back in the caller, is arr[0] changed?
Lesson 2·Foundations

Classes in Depth

You know what a class is. Now let's understand constructors, this, static vs instance members, method overloading, equals()/hashCode(), and how to design a class responsibly.

A class should represent exactly one concept. Fields store state. Methods define behavior. The constructor establishes a valid initial state — the object should be consistent from the moment new returns.

Constructor Chaining · static members · this

BankAccount.javaConstructor Chaining · static · equals · hashCode
public class BankAccount {

    // static: shared across ALL instances — counts total created
    private static int totalAccounts = 0;
    private static final double INTEREST_RATE = 0.035;

    private final String accountId;   // final = set once, never changed
    private String ownerName;
    private double balance;

    // Constructor 1 delegates to Constructor 2 via this(...)
    public BankAccount(String ownerName) {
        this(ownerName, 0.0); // must be FIRST statement
    }

    // Constructor 2: all initialization funnels here
    public BankAccount(String ownerName, double initialBalance) {
        if (initialBalance < 0) throw new IllegalArgumentException("Balance cannot be negative");
        this.ownerName = ownerName;
        this.balance   = initialBalance;
        totalAccounts++;
        this.accountId = "ACC-" + totalAccounts;
    }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount > balance) throw new IllegalStateException("Insufficient funds");
        balance -= amount;
    }

    public void applyInterest() { balance += balance * INTEREST_RATE; }

    // Static method: no 'this', can't touch instance fields
    public static int getTotalAccounts() { return totalAccounts; }

    public String getAccountId() { return accountId; }
    public double getBalance()    { return balance;   }

    // equals + hashCode: mandatory pair — required for use in Sets/Maps
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof BankAccount other)) return false; // Java 16 pattern
        return accountId.equals(other.accountId);
    }

    @Override
    public int hashCode() { return Objects.hash(accountId); }

    @Override
    public String toString() {
        return String.format("[%s] %s — $%.2f", accountId, ownerName, balance);
    }
}
The equals/hashCode Contract

If you override equals(), you MUST also override hashCode(). Objects that are equals() must produce the same hashCode. Violating this silently breaks HashSet and HashMap — two objects that should be "equal" get stored as separate entries. The bug is extremely hard to find at runtime.

Engineering Challenge

Design a Product Class for an E-Commerce System

Your class must: auto-increment id (final), validate price > 0 and stock ≥ 0 in constructor, implement purchase(int qty) that throws IllegalStateException on insufficient stock, applyDiscount(double pct) that validates 0–100, and override toString(), equals(), hashCode(). Add a static getTotalProducts(). Write a main that creates products and stores them in a HashSet — verifying that two products with the same ID count as one entry.

Knowledge Check
A static method inside a class can directly access instance fields of that class. True or false?
Lesson 3·Foundations

Encapsulation Done Right

Encapsulation isn't just "make fields private." It's designing a class so that it is impossible to put it into an invalid state. Blind getters and setters defeat the purpose entirely.

If you add a setBalance(double b) method with no validation, anyone can write account.setBalance(-999999). You've made the field private but given away full control. That's worse than public.

The rule: an object should be the sole guardian of its own consistency. All state transitions happen through methods that enforce business rules. Outside code tells the object what to do, not how to update its fields.

PlayerCharacter.javaDefensive Encapsulation · private levelUp()
public class PlayerCharacter {
    private String name;
    private int health, maxHealth, level, xp;
    private static final int XP_PER_LEVEL = 100;

    public PlayerCharacter(String name) {
        this.name = name; this.maxHealth = 100;
        this.health = maxHealth; this.level = 1;
    }

    public void takeDamage(int amount) {
        if (amount < 0) throw new IllegalArgumentException("Damage can't be negative");
        health = Math.max(0, health - amount); // can't go below 0
        if (isDead()) System.out.println(name + " has been defeated!");
    }

    public void heal(int amount) {
        if (isDead()) return; // dead characters can't heal
        health = Math.min(maxHealth, health + amount); // can't exceed max
    }

    public void gainXP(int amount) {
        xp += amount;
        while (xp >= XP_PER_LEVEL) { xp -= XP_PER_LEVEL; levelUp(); }
    }

    private void levelUp() { // PRIVATE — callers don't control this directly
        level++; maxHealth += 20; health = maxHealth;
        System.out.printf("%s reached level %d! Max HP: %d%n", name, level, maxHealth);
    }

    public boolean isDead()    { return health == 0; }
    public int     getHealth() { return health;    }
    public int     getLevel()  { return level;     }
}
🚫
The Anemic Domain Model Anti-Pattern

Classes that only have data (getters/setters) with no real behavior are called Anemic Domain Models. All logic ends up in external "service" classes that manipulate them. This is procedural code disguised as OOP. If your class looks like a struct, redesign it so the class itself owns its business logic.

Defensive Copying

Even with private fields, you can leak mutable references. If a class stores a List<String> and its getter returns it directly, callers can call .clear() on that list — silently corrupting your object's state from the outside.

Inventory.javaDefensive Copy Pattern
import java.util.*;

public class Inventory {
    private final List<String> items;

    public Inventory(List<String> initial) {
        this.items = new ArrayList<>(initial); // defensive copy on the way IN
    }

    public void addItem(String item) { items.add(item); }

    // ❌ BAD: leaks internal reference — caller can items.clear()
    public List<String> getItemsBAD() { return items; }

    // ✅ GOOD: return an unmodifiable view
    public List<String> getItems() { return Collections.unmodifiableList(items); }
}
Engineering Challenge

Build an Immutable Money Class

All fields final. No setters. add(Money other) returns a new Money (same currency required; throw on mismatch). multiply(double factor) returns a new Money. subtract(Money other) throws if result would be negative. Override equals(), hashCode(), toString(). This mirrors how Java's BigDecimal and String work — complete immutability.

Knowledge Check
Your class has a private List<String> tags. Your getter returns tags directly. A caller does obj.getTags().clear(). What happens?
Lesson 4·Core Pillars

Inheritance vs Composition

Inheritance is powerful and overused. You need to know not just how it works, but when to reach for it — and when composition is the better tool. This distinction separates junior from senior engineers.

The is-a vs has-a Rule

Use inheritance only when the relationship is genuinely is-a: a SavingsAccount IS a BankAccount. Use composition for has-a: a Car HAS an Engine — don't make Car extends Engine. The principle: "Favor composition over inheritance." Inheritance creates tight coupling — if the parent changes, children can silently break.

accounts/Inheritance · super() · @Override · Behavioral Extension
public class BankAccount {
    protected String id;
    protected double balance;

    public BankAccount(String id, double balance) {
        this.id = id; this.balance = balance;
    }

    public void withdraw(double amount) {
        if (amount > balance) throw new IllegalStateException("Insufficient funds");
        balance -= amount;
    }
    public double getBalance() { return balance; }
}

// Child adds overdraft capability
public class CheckingAccount extends BankAccount {
    private double overdraftLimit;

    public CheckingAccount(String id, double balance, double overdraftLimit) {
        super(id, balance); // MUST call parent constructor first
        this.overdraftLimit = overdraftLimit;
    }

    @Override
    public void withdraw(double amount) {
        if (amount > balance + overdraftLimit)
            throw new IllegalStateException("Exceeds overdraft limit");
        balance -= amount; // balance goes negative = into overdraft
    }
}

// Child adds interest + monthly withdrawal cap
public class SavingsAccount extends BankAccount {
    private double interestRate;
    private int withdrawalsThisMonth = 0;
    private static final int MAX_WITHDRAWALS = 6;

    public SavingsAccount(String id, double balance, double rate) {
        super(id, balance); this.interestRate = rate;
    }

    @Override
    public void withdraw(double amount) {
        if (withdrawalsThisMonth >= MAX_WITHDRAWALS)
            throw new IllegalStateException("Monthly withdrawal limit reached");
        super.withdraw(amount); // reuse parent's balance check — don't duplicate!
        withdrawalsThisMonth++;
    }

    public void applyMonthlyInterest() {
        balance += balance * interestRate;
        withdrawalsThisMonth = 0;
    }
}

Composition: When Not to Inherit

A Car isn't a specialized Engine. It HAS an engine, HAS a GPS. Compose — don't inherit. Composition is more flexible: you can swap the composed object at runtime, and there's no tight coupling to internal implementation details.

Car.javaComposition over Inheritance
public class Engine {
    private int hp;
    public Engine(int hp) { this.hp = hp; }
    public void start() { System.out.println("Engine started (" + hp + "hp)"); }
    public void stop()  { System.out.println("Engine stopped"); }
}

public class GPS {
    public void setDestination(String dest) {
        System.out.println("Routing to: " + dest);
    }
}

// Car HAS-A engine and HAS-A GPS — not IS-A Engine
public class Car {
    private final String model;
    private final Engine engine; // composed
    private final GPS gps;      // composed

    public Car(String model, int hp) {
        this.model  = model;
        this.engine = new Engine(hp);
        this.gps    = new GPS();
    }

    public void start()                        { engine.start();              }
    public void navigateTo(String destination) { gps.setDestination(destination); }
}
The Fragile Base Class Problem

When you inherit from a class, your subclass is tightly coupled to its internal implementation. If the parent changes a method that your super.method() calls, your behavior can silently break without any compile error. This is the "fragile base class problem" and it's a primary reason experienced engineers prefer composition for everything except true is-a relationships.

Engineering Challenge

Refactor: Inheritance → Composition + Strategy

You're given a poorly-designed hierarchy: Logger extends FileWriter extends BufferedStream. Refactor it: Logger should have-a OutputStrategy (interface). Implement ConsoleOutputStrategy and FileOutputStrategy. Logger accepts the strategy in its constructor and can have it swapped at runtime. Add a CompositeOutputStrategy that writes to multiple strategies simultaneously. This is the Strategy Pattern — preview of Lesson 8.

Knowledge Check
When should you choose inheritance over composition?
Lesson 5·Core Pillars

Polymorphism in Practice

Polymorphism is how you write code that works correctly with types you haven't invented yet. It powers plug-and-play architecture, frameworks, and testable systems.

Dynamic Dispatch — How Java Resolves Method Calls

When you call a method through a parent-type reference, Java resolves at runtime which overridden version to call based on the actual object type. This is dynamic dispatch, implemented via a virtual method table (vtable). The JVM, not the compiler, decides which method body runs.

PaymentProcessor.javaPolymorphism · Dynamic Dispatch · instanceof pattern matching
public abstract class PaymentMethod {
    protected String ownerName;

    public abstract boolean processPayment(double amount);
    public abstract String  getPaymentType();

    protected void printReceipt(double amount, boolean success) {
        System.out.printf("[%s] %s $%.2f — %s%n",
            getPaymentType(), ownerName, amount, success ? "APPROVED" : "DECLINED");
    }
}

public class CreditCard extends PaymentMethod {
    private double creditLimit, debt;
    public CreditCard(String owner, double limit) { ownerName = owner; creditLimit = limit; }

    @Override
    public boolean processPayment(double amount) {
        if (debt + amount > creditLimit) { printReceipt(amount, false); return false; }
        debt += amount; printReceipt(amount, true); return true;
    }
    public void payOffDebt(double amount) { debt = Math.max(0, debt - amount); }
    @Override public String getPaymentType() { return "CREDIT"; }
}

public class DebitCard extends PaymentMethod {
    private double balance;
    public DebitCard(String owner, double balance) { ownerName = owner; this.balance = balance; }

    @Override
    public boolean processPayment(double amount) {
        if (amount > balance) { printReceipt(amount, false); return false; }
        balance -= amount; printReceipt(amount, true); return true;
    }
    @Override public String getPaymentType() { return "DEBIT"; }
}

// The processor is completely agnostic about which subtype it receives
public class Checkout {
    public static void main(String[] args) {
        PaymentMethod[] methods = {
            new CreditCard("Alex", 2000),
            new DebitCard("Sam", 150)
        };

        for (PaymentMethod m : methods) {
            m.processPayment(200); // dynamic dispatch — right version runs

            // Pattern matching instanceof (Java 16+): access subtype-specific methods
            if (m instanceof CreditCard cc) {
                cc.payOffDebt(50); // cc is already cast — no explicit cast needed
            }
        }
    }
}
💡
Adding PayPal requires zero changes to Checkout

Create class PayPal extends PaymentMethod. Pass it into the array. It works immediately. This is the Open/Closed Principle — code is open for extension, closed for modification. You'll formalize this in the SOLID lesson.

Engineering Challenge

Build a Notification System with Runtime Extensibility

Abstract class Notification with abstract method send(String message). Implement EmailNotification, SMSNotification, PushNotification. NotificationService holds a List<Notification> and broadcast(String msg) sends to all. Then add a new SlackNotification — verifying that NotificationService requires zero changes. Add a sendFiltered(String msg, Predicate<Notification> filter) method using lambdas.

Knowledge Check
You have Animal a = new Dog(). Which version of makeSound() is called — and when is that decided?
Lesson 6·Core Pillars

Abstraction: Abstract Classes vs Interfaces

Both define contracts. Knowing which to use — and when to use them together — is a mark of engineering maturity. The distinction is subtle but architecturally critical.

Abstract ClassInterface
Instance fieldsYesOnly static final constants
ConstructorYesNo
Concrete methodsYesOnly default/static (Java 8+)
A class can use how manyOne (extends)Many (implements A, B, C)
Best forShared state + behavior + partial implementationCapability contracts (can-do)
media/Abstract Class + Multiple Interfaces + Template Method Pattern
// Interfaces — define capabilities
public interface Playable     { void play(); void pause(); void stop(); }
public interface Downloadable { void download(String path); long getSizeBytes(); }

public interface Shareable {
    String getContentId();
    // Default method: interfaces CAN have implementations since Java 8
    default String generateShareLink() {
        return "https://share.app/" + getContentId();
    }
}

// Abstract class: shared state + Template Method Pattern
public abstract class MediaContent implements Playable, Shareable {
    protected final String id, title, author;
    protected boolean isPlaying = false;

    public MediaContent(String id, String title, String author) {
        this.id = id; this.title = title; this.author = author;
    }

    @Override public String getContentId() { return id; }

    // Template Method: defines the SKELETON — subclasses fill in onPlay()
    @Override
    public void play() {
        if (!isPlaying) { isPlaying = true; onPlay(); } // hook
    }

    @Override public void stop()  { isPlaying = false; System.out.println("⏹ Stopped"); }
    @Override public void pause() { isPlaying = false; System.out.println("⏸ Paused");  }

    protected abstract void onPlay();           // subclasses define this
    public abstract int     getDurationSeconds();
}

public class Video extends MediaContent implements Downloadable {
    private int durationSec, resolutionP;

    public Video(String id, String title, String author, int dur, int res) {
        super(id, title, author); durationSec = dur; resolutionP = res;
    }

    @Override protected void onPlay() {
        System.out.printf("▶ Playing '%s' in %dp%n", title, resolutionP);
    }
    @Override public int  getDurationSeconds()   { return durationSec; }
    @Override public void download(String path)  { System.out.println("Downloading to " + path); }
    @Override public long getSizeBytes()          { return (long) durationSec * resolutionP * 1000; }
}
🧩
Template Method Pattern — Built into the Abstraction

play() defines the algorithm skeleton and delegates to the abstract hook onPlay(). Subclasses implement the hook without changing the overall flow or the bookkeeping logic. This is the Template Method design pattern — you'll find it in HttpServlet.doGet(), JUnit test lifecycle, and most major Java frameworks.

Engineering Challenge

Design a Swappable Database Connector

Interface DatabaseConnection: connect(), disconnect(), executeQuery(String sql), isConnected(). Abstract class AbstractDatabaseConnection implements shared logic (connection state tracking, query logging, timing). Concrete classes MySQLConnection and PostgreSQLConnection. A DataService depends only on DatabaseConnection — never a concrete class. Switch implementations without touching DataService.

Knowledge Check
An interface can contain concrete method bodies using which keyword (Java 8+)?
Lesson 7·Advanced

SOLID Principles

SOLID is five design principles that make OOP code maintainable, testable, and scalable. Each principle fixes a specific way class design goes wrong in real codebases.

S

Single Responsibility Principle

A class should have one — and only one — reason to change. If a class handles data persistence AND business logic, a database schema change forces edits to your business logic file. Split them.

O

Open/Closed Principle

Open for extension, closed for modification. Add new behaviors by writing new classes, not by editing existing ones. The payment processor in Lesson 5 is a perfect example: add PayPal by extending, not modifying.

L

Liskov Substitution Principle

A subclass must be fully substitutable for its parent without breaking the program. If code expects a BankAccount and receives a SavingsAccount, everything must work correctly — no surprises, no thrown exceptions that the parent wouldn't throw.

I

Interface Segregation Principle

Don't force classes to implement methods they don't need. A fat interface with 20 methods forces every implementer to stub out methods they don't use. Split it into small, focused interfaces.

D

Dependency Inversion Principle

High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces). Your business logic should depend on an OrderRepository interface — not on MySQLDatabase directly.

solid/DependencyInversion.javaDIP + Constructor Injection + Testability
// ❌ BAD: high-level class hard-codes a low-level dependency
public class OrderService_BAD {
    private MySQLDatabase db = new MySQLDatabase(); // impossible to swap
    public void saveOrder(Order o) { db.insert(o); }  // untestable without a real DB
}

// ✅ GOOD: depend on an abstraction
public interface OrderRepository {
    void   save(Order order);
    Order  findById(int id);
    void   delete(int id);
    List<Order> findAll();
}

public class OrderService {
    private final OrderRepository repo; // interface, not concrete class

    public OrderService(OrderRepository repo) { // INJECTED, not created here
        this.repo = repo;
    }

    public void placeOrder(Order order) {
        order.validate();
        repo.save(order);
        System.out.println("Order placed: " + order);
    }

    public List<Order> getAllOrders() { return repo.findAll(); }
}

// Production: real MySQL implementation
public class MySQLOrderRepository implements OrderRepository {
    @Override public void save(Order o)       { /* MySQL INSERT */ }
    @Override public Order findById(int id)   { return null; /* MySQL SELECT */ }
    @Override public void delete(int id)       { /* MySQL DELETE */ }
    @Override public List<Order> findAll()    { return new ArrayList<>(); }
}

// Tests: fast in-memory implementation — no DB required
public class InMemoryOrderRepository implements OrderRepository {
    private final Map<Integer, Order> store = new HashMap<>();
    @Override public void save(Order o)       { store.put(o.getId(), o);  }
    @Override public Order findById(int id)   { return store.get(id);     }
    @Override public void delete(int id)       { store.remove(id);         }
    @Override public List<Order> findAll()    { return new ArrayList<>(store.values()); }
}

// Wiring — swap implementations with zero changes to OrderService
public class Main {
    public static void main(String[] args) {
        // Production:
        OrderService prod = new OrderService(new MySQLOrderRepository());
        // Tests:
        OrderService test = new OrderService(new InMemoryOrderRepository());
    }
}
🔑
Why DIP Makes Testing Easy

In a unit test, inject InMemoryOrderRepository instead of a real database. Tests run in milliseconds with no setup. This is the foundation of testable architecture. Frameworks like Spring do this injection automatically — now you understand what they're actually doing under the hood.

Engineering Challenge

Audit and Refactor a God Class

Start with a UserManager that: validates user input, hashes passwords, saves to database, sends welcome email, and logs everything. Identify every SOLID violation. Refactor into:

  • UserValidator — validates inputs
  • PasswordHasher — hashing logic
  • UserRepository (interface) + InMemoryUserRepository
  • EmailService (interface) + SmtpEmailService
  • UserService — orchestrates, depends only on interfaces via constructor injection

After refactoring: add a MockEmailService for tests that records sent emails without sending real ones.

Knowledge Check
Which scenario violates the Liskov Substitution Principle?
Lesson 8·Advanced

Design Patterns

Design patterns are proven, named solutions to recurring problems. Knowing them lets you communicate in one word — "use a Builder," "that's Observer" — and avoid reinventing solutions that have been battle-tested for decades. This lesson is a working reference; the dedicated Design Patterns lesson covers all six in full depth, including three different ways to implement a Singleton correctly.

Creational

Builder

Construct complex objects step-by-step with many optional parameters. Fluent API. Private constructor.

Creational

Factory Method

Delegate object creation to subclasses or a factory. Hides instantiation complexity behind an interface.

Creational

Singleton

Ensure only one instance exists globally. Used for config managers, thread pools, loggers. Handle with care.

Behavioral

Strategy

Encapsulate a family of algorithms. Make them interchangeable at runtime without changing the context class.

Behavioral

Observer

One-to-many dependency: when one object changes state, all registered dependents are notified automatically.

Structural

Decorator

Wrap an object to add behavior dynamically. More flexible than subclassing — stack decorators like layers.

patterns/Builder.javaBuilder Pattern — HTTP Request
// Problem: constructors with 8+ params are unreadable and error-prone.
// Builder gives a fluent, validated, immutable construction API.
public class HttpRequest {
    private final String             url;
    private final String             method;
    private final Map<String,String> headers;
    private final String             body;
    private final int                 timeoutMs;

    private HttpRequest(Builder b) { // private — only Builder can create
        url = b.url; method = b.method;
        headers = Collections.unmodifiableMap(b.headers);
        body = b.body; timeoutMs = b.timeoutMs;
    }

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

        public Builder(String url) { this.url = url; }

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

        public HttpRequest build() {
            if (url == null || url.isBlank()) throw new IllegalStateException("URL required");
            return new HttpRequest(this);
        }
    }
}

// Fluent, readable usage — clearly shows what each argument IS
HttpRequest req = new HttpRequest.Builder("https://api.example.com/orders")
    .method("POST")
    .header("Authorization", "Bearer token123")
    .header("Content-Type", "application/json")
    .body("{\"item\":\"book\",\"qty\":2}")
    .timeout(10_000)
    .build();
📖
Want Observer, Factory Method, Decorator, and Singleton with full working code?

This lesson keeps one worked example (Builder) so you can see a real pattern in context. For the complete treatment of all six patterns — including a thread-safe Singleton and the modern enum-based Singleton — head to the dedicated Design Patterns lesson.

Engineering Challenge

Plugin Architecture using Factory + Strategy

Build an image processing pipeline. Each filter (Grayscale, Blur, Sharpen, Resize, Brightness) is a Strategy (interface ImageFilter with apply(Image img)). A FilterFactory takes a string name and returns the right implementation — throwing IllegalArgumentException for unknown names. An ImageProcessor accepts a List<ImageFilter> and applies them in sequence. Registering a new filter requires zero changes to existing code — demonstrating OCP. Bonus: implement a PipelineBuilder using the Builder pattern to construct filter chains fluently.

Knowledge Check
The Builder pattern primarily solves which problem?
Lesson 9·Advanced

Generics & Collections

Generics let you write type-safe classes and methods that work with any type. They're the engine behind ArrayList, HashMap, and every Java collection. Understanding them makes everything in the standard library click.

Without generics, you'd store everything in List<Object> and cast on every retrieval — error-prone and ugly. Generics give the compiler full type information: "this list holds only Strings" — so bugs surface at compile time, not runtime.

Generic Classes with Bounded Type Parameters

generics/Repository.javaGeneric Classes · <T extends Interface> · Type Safety
// Everything stored in the repository must have an ID
public interface Identifiable {
    int getId();
}

// T extends Identifiable — we can call getId() inside the class
public class GenericRepository<T extends Identifiable> {

    private final Map<Integer, T> store = new HashMap<>();

    public void     save(T entity)  { store.put(entity.getId(), entity); }
    public T        findById(int id) {
        T result = store.get(id);
        if (result == null) throw new NoSuchElementException("Not found: " + id);
        return result;
    }
    public List<T>  findAll()       { return new ArrayList<>(store.values()); }
    public void     delete(int id)  { store.remove(id);  }
    public int      count()         { return store.size(); }

    // Generic method — finds first entity matching a predicate
    public Optional<T> findFirst(Predicate<T> predicate) {
        return store.values().stream().filter(predicate).findFirst();
    }
}

// Fully type-safe for any Identifiable — no casts, no Object[]
GenericRepository<User>    users    = new GenericRepository<>();
GenericRepository<Product> products = new GenericRepository<>();

users.save(new User(1, "Alex"));
User u = users.findById(1); // no cast needed!
// users.save(new Product(...)); // COMPILE ERROR — wrong type
collections/StreamsDemo.javaCollections Framework · Stream API · Collectors
import java.util.*;
import java.util.stream.*;

public class CollectionsDemo {
    record Employee(String name, String dept, double salary) {}

    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("Alice", "Eng", 120_000),
            new Employee("Bob",   "Eng",  95_000),
            new Employee("Carol", "HR",   80_000),
            new Employee("Dave",  "Eng", 110_000)
        );

        // Filter + map + collect
        List<String> highEarners = employees.stream()
            .filter(e -> e.salary() > 100_000)
            .map(Employee::name)
            .sorted()
            .collect(Collectors.toList());
        // [Alice, Dave]

        // Group by department
        Map<String, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::dept));
        // {Eng: [Alice, Bob, Dave], HR: [Carol]}

        // Average salary by department
        Map<String, Double> avgSalary = employees.stream()
            .collect(Collectors.groupingBy(
                Employee::dept,
                Collectors.averagingDouble(Employee::salary)
            ));
        // {Eng: 108333.33, HR: 80000.0}

        // Find highest-paid engineer
        Optional<Employee> topEng = employees.stream()
            .filter(e -> e.dept().equals("Eng"))
            .max(Comparator.comparingDouble(Employee::salary));
        topEng.ifPresent(e -> System.out.println("Top: " + e.name())); // Alice
    }
}
CollectionWhen to useKey characteristic
ArrayListIndexed access, iterationO(1) get, O(n) insert middle
LinkedListFrequent front/back insertionsO(1) add/remove at ends
HashMapKey-value lookupO(1) average get/put
LinkedHashMapKey-value + insertion orderPredictable iteration order
TreeMapSorted key-value pairsO(log n), keys sorted
HashSetUnique elements, fast lookupO(1) contains
TreeSetSorted unique elementsO(log n), auto-sorted
PriorityQueueAlways need min/max nextO(log n) poll, O(1) peek
Engineering Challenge

Build a Type-Safe Generic Event Bus

Create EventBus<T> with subscribe(Consumer<T> handler) and publish(T event). Create UserCreatedEvent and OrderPlacedEvent. Create two separate buses — one per type. Prove at compile time that a handler registered on the user bus cannot receive order events. Then extend it: publish(T event, Predicate<T> filter) that only delivers to handlers whose filter returns true. Use the Stream API internally. Bonus: make it thread-safe using CopyOnWriteArrayList for the observer list.

Knowledge Check
You need to look up values by key millions of times per second. Which collection gives the best average-case performance?
Lesson 10·Capstone Project

Capstone: Build a Trading Platform

This multi-class engineering project integrates every concept from the course into a coherent real-world system. Design decisions are yours — there's no single right answer.

📋
System Overview

You're building the core engine of a stock trading platform. Users open accounts, place buy/sell orders (market, limit, stop-loss), and receive notifications when orders execute. Stock prices change and trigger alerts. An order book matches buyers to sellers.

Entities

User, Account, Order, Stock

Encapsulated, immutable IDs, validation in constructors, equals/hashCode/toString.

Hierarchy

Order Types

Abstract OrderMarketOrder, LimitOrder, StopLossOrder. Each executes differently.

Interfaces

Capability Contracts

Executable, Cancellable, Notifiable. Not all order types implement all interfaces.

SOLID

Services

OrderService, PriceService, NotificationService. Single responsibility, injected via interface.

Patterns

Design Patterns Used

Builder (Order construction), Observer (price alerts), Factory (order types), Strategy (execution logic).

Generics

Type-Safe Collections

Generic Repository<T>. PriorityQueue for order book. Stream API throughout.

trading/Order.javaCapstone Starter — Core Abstract Class
public abstract class Order implements Identifiable, Comparable<Order> {

    public enum Side   { BUY, SELL }
    public enum Status { PENDING, PARTIALLY_FILLED, FILLED, CANCELLED }

    private static int nextId = 1000;

    private final int    id;
    private final String ticker;
    private final Side   side;
    private final int    quantity;
    private int          filledQty = 0;
    private Status       status    = Status.PENDING;
    private final long   createdAt;

    protected Order(String ticker, Side side, int quantity) {
        if (quantity <= 0) throw new IllegalArgumentException("Quantity must be positive");
        this.id        = nextId++;
        this.ticker    = ticker;
        this.side      = side;
        this.quantity  = quantity;
        this.createdAt = System.currentTimeMillis();
    }

    // Subclasses define their own execution condition
    public abstract boolean canExecuteAt(double marketPrice);
    public abstract String  getOrderType();

    public void fill(int qty) {
        if (status == Status.CANCELLED) throw new IllegalStateException("Can't fill cancelled order");
        filledQty += qty;
        status = (filledQty >= quantity) ? Status.FILLED : Status.PARTIALLY_FILLED;
    }

    public void cancel() {
        if (status == Status.FILLED) throw new IllegalStateException("Can't cancel filled order");
        status = Status.CANCELLED;
    }

    @Override
    public int compareTo(Order other) { // FIFO by default — override in subclasses for price priority
        return Long.compare(this.createdAt, other.createdAt);
    }

    @Override public int    getId()         { return id;        }
    public String           getTicker()     { return ticker;    }
    public Side             getSide()       { return side;      }
    public int              getQuantity()   { return quantity;  }
    public int              getFilledQty()  { return filledQty; }
    public Status           getStatus()     { return status;    }

    @Override
    public String toString() {
        return String.format("[#%d %s %s %dx %s | %s]",
            id, getOrderType(), side, quantity, ticker, status);
    }
}

// ─── Provided implementations ──────────────────────────
public class MarketOrder extends Order {
    public MarketOrder(String ticker, Side side, int qty) { super(ticker, side, qty); }
    @Override public boolean canExecuteAt(double p) { return true; } // always fills
    @Override public String  getOrderType()            { return "MARKET"; }
}

public class LimitOrder extends Order {
    private final double limitPrice;
    public LimitOrder(String ticker, Side side, int qty, double limitPrice) {
        super(ticker, side, qty); this.limitPrice = limitPrice;
    }
    @Override
    public boolean canExecuteAt(double price) {
        return getSide() == Side.BUY  ? price <= limitPrice
             : getSide() == Side.SELL ? price >= limitPrice : false;
    }
    @Override public String getOrderType() { return "LIMIT@$" + limitPrice; }
}

// ─── YOUR TASK ────────────────────────────────────────────
// 1. Implement StopLossOrder — executes when price DROPS BELOW trigger
// 2. Implement OrderBook using PriorityQueue — matches buys to sells
// 3. Implement a Builder for LimitOrder construction
// 4. Apply Observer pattern: Stock notifies OrderBook on price changes
// 5. Build OrderService with injected GenericRepository<Order>
Evaluation CriterionWhat to Look For
EncapsulationNo public mutable fields; all state changes through validated methods
Inheritanceis-a relationships only; super() used correctly; no duplicated logic
PolymorphismOrder engine iterates over Order references — works with any subtype added later
SOLID — SRPEach service class has exactly one job; no god classes
SOLID — OCPAdding StopLossOrder requires zero changes to OrderBook/OrderService
SOLID — DIPAll services injected via constructor; none create their own dependencies
Builder PatternLimitOrder.Builder with fluent API and validation in build()
Observer PatternStock notifies subscribers; OrderBook subscribes to trigger executions
Generics + CollectionsGenericRepository<T> used; PriorityQueue for order book; Streams for queries
🏁
Where to Go from Here

With a strong OOP and design-patterns foundation, your next milestones are: Exception Hierarchies (custom checked/unchecked exceptions), Concurrency (thread-safe objects, locks, synchronized, ExecutorService), Functional Java (lambdas, method references, Optional in depth), Unit Testing with JUnit 5 (your SOLID-compliant code is now fully testable), and Spring Framework — which is built entirely on DIP and the patterns you now understand.

Roadmap