Lesson 1 of 109 · Module 1: Classes, objects and responsibilities · about 25 min theory + exercises


1. Learning Objective

A. What you will learn

By the end of this lesson you should be able to:

  • i. Explain what Low Level Design (LLD) is.
  • ii. Explain how LLD differs from High Level Design (HLD).
  • iii. Describe a small piece of software as objects with responsibilities.

B. Why it matters

  • i. Every later lesson (SOLID, patterns, Parking Lot, Splitwise) builds on this way of thinking.
  • ii. Interviews test whether you can reason from requirements, not whether you remember code.

C. What you will build

  • i. A BankAccount that protects its own rules.
  • ii. A ShoppingCart (Exercise 1).
  • iii. A discount calculator using a first pattern, Strategy (Exercise 2).

D. Prerequisites

You should already understand classes, methods, fields and constructors. Examples use Java.


2. The Problem

A. The situation

Your team is told: "Build a banking app." Two engineers start.

  • i. Engineer 1 draws boxes: mobile app, API gateway, database, cache, queue.
  • ii. Engineer 2 opens an editor and asks: what classes exist, who may change a balance, what happens on an overdraw?

B. The observation

Both are designing, but at different levels. Engineer 1 is doing HLD. Engineer 2 is doing LLD.

C. The question this lesson answers

What exactly is LLD, and what does "thinking in objects" mean?

D. Why a beginner gets stuck

  • i. A beginner sees only data (accountNumber, balance) and functions that modify it.
  • ii. Nobody owns the rules, so the rules end up scattered across the code.

3. Concept: HLD vs LLD

A. Side-by-side comparison

High Level Design (HLD) Low Level Design (LLD)
Zoom level Whole system Inside one service or module
Building blocks Services, databases, queues, caches Classes, interfaces, objects, methods
Core question What are the big parts and how do they communicate? What are the objects, who owns what, how do they collaborate?
Typical output Architecture diagram Class model, interfaces, code structure

B. One example, two levels

HLD  →  "We need an Account Service and a Notification Service."
LLD  →  "Inside Account Service: Account, Transaction, Money, AccountRepository..."

C. What LLD decides

  • i. Which objects exist.
  • ii. What each object knows (state) and does (behavior).
  • iii. Who is responsible for what.
  • iv. How objects depend on and talk to each other.
  • v. How easily the design absorbs a changing requirement.

D. Thinking in objects

  • i. Data thinking: accountNumber = "ACC-1", balance = 1200, with functions that change them from anywhere.
  • ii. Object thinking: one BankAccount that owns its data and protects its own rules.
BankAccount
  state:     accountNumber, balance
  behavior:  deposit(), withdraw(), getBalance()
  rule:      balance can never go negative
  • iii. The shift: data and the rules about that data live together.

4. Bad Design

A. The naive class

Anyone can reach in and change the balance.

public class BankAccount {
    public String accountNumber;
    public double balance;
}

B. How it gets misused

account.balance = account.balance - 5000;   // overdrawn, nobody stopped it
account.balance = -999;                      // nonsense state, nobody stopped it

C. Why it is bad

  • i. The rule "balance cannot go negative" is written nowhere. Every caller must remember it.
  • ii. If the rule changes (say, an overdraft limit), you must hunt down every place that touches balance.
  • iii. The class is a bag of data with no responsibilities, so responsibility scatters across the codebase.

D. The key question

Who should enforce the rules of an account? The object that owns the data.


5. Better Design

A. The improved class

public class BankAccount {
    private final String accountNumber;
    private double balance;

    public BankAccount(String accountNumber, double openingBalance) {
        if (openingBalance < 0) {
            throw new IllegalArgumentException("Opening balance cannot be negative");
        }
        this.accountNumber = accountNumber;
        this.balance = openingBalance;
    }

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

    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Withdrawal must be positive");
        }
        if (amount > balance) {
            throw new IllegalStateException("Insufficient funds");
        }
        balance -= amount;
    }

    public double getBalance() { return balance; }
    public String getAccountNumber() { return accountNumber; }
}

B. What changed

  • i. Fields are private, so outside code cannot touch them.
  • ii. State changes only through deposit and withdraw.
  • iii. The constructor refuses an invalid starting state.

C. The result

  • i. The withdrawal rule lives in exactly one place.
  • ii. It is impossible to put the account into an invalid state from outside.
  • iii. This idea is called encapsulation. We study it properly in Lesson 4.

D. How we arrived at this design

Ask these questions, in this order:

What object is this?             → BankAccount
What does it know?               → accountNumber, balance
What can it do?                  → deposit, withdraw
What rules must always hold?     → balance >= 0, amounts > 0
Who should enforce those rules?  → the object that owns the data

6. Small Exercise 1: Design Before You Code

A. Requirement

A ShoppingCart holds items. A customer can add an item, remove an item, and see the total price.

B. Step one: design on paper, no code yet

Fill in this table:

Question Your answer
What objects exist?
What does each object know (state)?
What can each object do (behavior)?
Who owns the list of items?
Who calculates the total?
What rule must always hold?

C. Step two: implement

  • i. Write CartItem and ShoppingCart in Java.
  • ii. Keep fields private.
  • iii. Put validation inside the classes, not in the callers.

D. Self-check

  • i. Can outside code add a negative quantity?
  • ii. Can outside code edit the item list directly? (Try returning an unmodifiable view.)
  • iii. If the total calculation changes, how many classes do you edit?

7. Introduction to a Design Pattern: Strategy (Preview)

Note: your roadmap teaches patterns in Week 5, after learners have felt the problems they solve. Today is a preview only. Do not memorize it. We return to Strategy in Lesson 40, and again in Discount Engine, Parking Lot and Payment System.

A. The problem

The bank now pays monthly interest. Savings accounts get 4% a year, student accounts 5%, current accounts 0%.

Naive solution:

public double monthlyInterest(double balance, String accountType) {
    if (accountType.equals("SAVINGS")) {
        return balance * 0.04 / 12;
    } else if (accountType.equals("STUDENT")) {
        return balance * 0.05 / 12;
    } else if (accountType.equals("CURRENT")) {
        return 0;
    }
    throw new IllegalArgumentException("Unknown account type");
}

B. Why the naive solution breaks

Marketing launches a "Senior Citizen" account.

  • i. You must open and edit working code. Every edit risks breaking the existing cases.
  • ii. Every new type grows the if/else chain.
  • iii. The logic depends on a string, so a typo like "SAVNGS" is only caught at runtime.
  • iv. You cannot test one interest rule in isolation.

The LLD question: what is changing? Only the way interest is calculated. Deposit, balance and account number stay still.

C. The pattern

Strategy: pull the part that varies into its own interface. The object holds an implementation and delegates to it.

          InterestStrategy  (interface)
                 ↑
     ┌───────────┼───────────┐
SavingsInterest  StudentInterest  CurrentInterest
public interface InterestStrategy {
    double monthlyInterest(double balance);
}

public class SavingsInterest implements InterestStrategy {
    public double monthlyInterest(double balance) { return balance * 0.04 / 12; }
}

public class StudentInterest implements InterestStrategy {
    public double monthlyInterest(double balance) { return balance * 0.05 / 12; }
}

public class CurrentInterest implements InterestStrategy {
    public double monthlyInterest(double balance) { return 0; }
}

The account holds a strategy and delegates:

public class InterestBearingAccount {
    private final String accountNumber;
    private double balance;
    private final InterestStrategy interestStrategy;

    public InterestBearingAccount(String accountNumber, double openingBalance,
                                  InterestStrategy interestStrategy) {
        if (openingBalance < 0) {
            throw new IllegalArgumentException("Opening balance cannot be negative");
        }
        this.accountNumber = accountNumber;
        this.balance = openingBalance;
        this.interestStrategy = interestStrategy;
    }

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

    public double monthlyInterest() {
        return interestStrategy.monthlyInterest(balance);
    }

    public double getBalance() { return balance; }
}

Using it:

InterestBearingAccount savings = new InterestBearingAccount("S-1", 12000, new SavingsInterest());
InterestBearingAccount current = new InterestBearingAccount("C-1", 12000, new CurrentInterest());

// Senior citizen account: no existing class edited, just a new strategy.
InterestBearingAccount senior =
    new InterestBearingAccount("U-1", 12000, balance -> balance * 0.06 / 12);

System.out.println(savings.monthlyInterest()); // 40.0
System.out.println(current.monthlyInterest()); // 0.0
System.out.println(senior.monthlyInterest());  // 60.0

Adding a new account type now means adding code, not editing working code.

D. When to use it, when not to, and how to spot it

  • i. Use it when one job can be done in several interchangeable ways and new ways keep appearing.
  • ii. Do not use it when there is one way and it is unlikely to change. With a single interest rule, an interface plus class is overengineering (YAGNI and KISS, covered in Week 3).
  • iii. Recognition clues: a growing if / else if or switch on a type or mode, the phrase "depending on X, calculate it differently", or needing to swap behavior at runtime or in tests.
  • iv. Real-world examples: Java's Comparator, choosing card vs UPI at checkout, choosing the route type in a maps app.

8. Small Exercise 2: Discount Strategy

A. Start with the naive version

Write a Cart with double finalPrice(double total, String customerType) using an if/else chain:

  • i. REGULAR pays full price.
  • ii. MEMBER gets 10% off.
  • iii. FESTIVAL gets 20% off.

B. Feel the pain

Add a fourth type, STAFF (30% off). Note exactly what you had to edit.

C. Refactor to Strategy

  • i. Create a DiscountStrategy interface with apply(double total).
  • ii. Create one implementation per customer type.
  • iii. Make Cart hold a DiscountStrategy and delegate to it.

D. Prove the design works

  • i. Add FIRST_ORDER (flat 100 off) without editing the Cart class.
  • ii. Success check: Cart contains no if on customer type.

9. Can You Explain?

A. HLD vs LLD

In your own words, what is the difference? Give one example decision from each.

B. Encapsulation effect

Why does BankAccount with a private balance and a withdraw method beat a class with a public balance field?

C. What is changing?

In the naive interest code, what is changing, and why does that question point toward Strategy?

D. Self-test rule

Answer all three without looking at the lesson. Note which ones you could not answer.


10. Can You Design?

A. Given

A Library lends books to members.

B. Task (no code)

  • i. List the objects.
  • ii. For each, list what it knows and what it does.

C. Rule hunt

Name at least one rule some object must enforce, and say which object.

D. Note

We revisit this exact problem in Week 2, so keep your answer.


11. Completion Criteria

A. Concepts

  • i. I can explain HLD vs LLD to a friend in under a minute.
  • ii. I can say what "thinking in objects" means.

B. Skills

  • i. I can list objects, state, behavior and rules before writing code.
  • ii. I can spot an if/else chain on a "type" and say what is changing.

C. Code

  • i. BankAccount and ShoppingCart cannot be put in an invalid state from outside.
  • ii. Exercise 2 is done and Cart has no if on customer type.

D. Reflection

I can explain why a pattern should wait until I feel the problem it solves.


12. Recall

A. Schedule

Revisit this lesson after 1 day, 3 days and 7 days.

B. What to do each time

Answer the three "Can You Explain?" questions without looking.

C. Where this resurfaces

  • i. Lesson 4: Encapsulation.
  • ii. Lesson 6: Responsibility.
  • iii. Lesson 21: Open/Closed Principle.
  • iv. Lesson 40: Strategy.
  • v. Discount Engine and Parking Lot exercises.

D. Next

Day 2 — Classes vs objects: thinking in objects rather than data structures.

Your notes

Notes & links

Saved automatically.

What is LLD | Low Level Design | ProgVale