JavaBook
Chapter 23· Object-Oriented Programming

OOP Design

52 min read16 diagrams

Java Master Course — Chapter 23 of 50

In Chapters 11–22, you learned how Java supports object-oriented programming:

Output
OOP Fundamentals
Classes & Objects
Constructors
this & static
Encapsulation
Inheritance
Overloading
Overriding
Polymorphism
Abstraction
Interfaces
OOP Relationships

Now comes one of the most important OOP topics:

How do we design good classes and objects?

Knowing Java syntax is not enough.

You can write code that compiles and still create a terrible design.

Good OOP design is about deciding:

Output
What should a class represent?
What responsibility should it have?
What should it hide?
What should it expose?
Which objects should know about each other?
Which dependencies should be injected?
Should we use inheritance or composition?
How can we keep the code easy to change?

This chapter introduces the principles that help answer those questions.


1. What You Will Learn#

By the end of this chapter, you should understand:

  • What software design means
  • What good OOP design means
  • Responsibility of a class
  • Single Responsibility Principle
  • Cohesion
  • Coupling
  • Low coupling
  • High cohesion
  • Encapsulation as a design tool
  • Programming to abstractions
  • Dependency inversion basics
  • Open/Closed Principle
  • Liskov Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle
  • Complete SOLID overview
  • Composition over inheritance
  • Favoring small focused classes
  • Immutable objects
  • final fields
  • Defensive copying
  • Unmodifiable collections
  • Value objects
  • Anemic vs behavior-rich objects
  • Tell, Don't Ask
  • Law of Demeter basics
  • Avoiding God classes
  • Avoiding deep inheritance
  • Avoiding excessive abstraction
  • Avoiding unnecessary interfaces
  • Avoiding unnecessary getters/setters
  • Common OOP design mistakes
  • Refactoring a bad design
  • Practical Java programs
  • Exercises
  • Output questions
  • Interview questions
  • Design challenges
  • Mini projects

2. What Is Software Design?#

Software design is the process of deciding how your program should be structured.

It includes decisions about:

Output
classes
objects
responsibilities
relationships
dependencies
interfaces
data
behavior

For example, imagine an online shopping application.

You could put everything into:

Java
class ShoppingApp {
    // 3000 lines
}

It may compile.

But it would be difficult to:

Output
understand
test
modify
debug
reuse
extend

Good design divides responsibilities into meaningful components.


3. Bad Design Can Still Compile#

This is very important.

The compiler checks things such as:

Output
syntax
types
method calls
access rules

It does not automatically tell you:

Output
"This class has too many responsibilities."

"This design is tightly coupled."

"You should use composition."

"This class is difficult to test."

"This interface is badly designed."

These are software-design problems.


4. The Goal of Good OOP Design#

Good OOP design generally tries to make software:

Output
easy to understand
easy to change
easy to test
easy to extend
easy to reuse
harder to break accidentally

A useful mental model is:

Good design
    ↓
clear responsibilities
    ↓
appropriate relationships
    ↓
manageable dependencies
    ↓
easier changes

5. Design Is About Managing Change#

Suppose your application currently supports:

Output
UPI

Tomorrow you need:

Output
Credit Card

Later:

Output
Wallet

A good design allows new payment types without rewriting the entire application.

This is one reason abstractions and polymorphism are useful.


6. A Simple Design Example#

Poor design:

Java
class OrderService {

    void process() {

        // calculate price
        // validate customer
        // save order
        // send email
        // process payment
        // generate invoice
    }
}

The class is doing too many unrelated things.

A better design might separate:

Output
OrderValidator
OrderCalculator
OrderRepository
PaymentService
EmailService
InvoiceGenerator

Now each class has a clearer responsibility.


7. Responsibility#

A class should have a clear reason to exist.

For example:

Java
class Invoice {

    double calculateTotal() {
        // invoice calculation
    }
}

Invoice is related to invoice behavior.

Compare:

Java
class Invoice {

    double calculateTotal() {
    }

    void sendEmail() {
    }

    void saveToDatabase() {
    }

    void connectToPaymentGateway() {
    }
}

Now the class is taking on unrelated responsibilities.


8. Single Responsibility Principle#

The first SOLID principle is:

Output
S — Single Responsibility Principle

A common simple explanation is:

A class should have one reason to change.

This does not mean:

Output
A class must contain exactly one method.

It means its responsibilities should be focused.


9. Why "One Reason to Change" Matters#

Suppose:

Java
class Invoice {

    void calculateTotal() {
    }

    void saveToDatabase() {
    }

    void sendEmail() {
    }
}

There are at least three different reasons for changing this class:

Output
pricing rules change
database logic changes
email requirements change

That is a warning sign.


10. Better SRP Design#

Separate responsibilities:

Java
class Invoice {

    double calculateTotal() {
        return 0;
    }
}

class InvoiceRepository {

    void save(Invoice invoice) {
    }
}

class InvoiceEmailService {

    void send(Invoice invoice) {
    }
}

Now:

Output
Invoice
→ invoice behavior

InvoiceRepository
→ persistence

InvoiceEmailService
→ email

11. SRP Does Not Mean "One Class Per Line of Code"#

Do not overreact.

Bad extreme:

Output
AddService
SubtractService
MultiplyService
DivisionService

for a tiny calculator where one cohesive class is perfectly reasonable.

The goal is not to create thousands of classes.

The goal is:

Output
meaningful responsibility boundaries

12. Cohesion#

Cohesion describes how closely related the responsibilities inside a class are.

High cohesion:

Java
class BankAccount {

    deposit()
    withdraw()
    getBalance()
}

These operations are strongly related.

Low cohesion:

Java
class Utility {

    sendEmail()
    calculateTax()
    resizeImage()
    saveDatabase()
    printReport()
}

These responsibilities are unrelated.


13. High Cohesion#

Think:

Class
 ├── related responsibility
 ├── related behavior
 └── related data

Example:

Java
class ShoppingCart {

    addItem()
    removeItem()
    calculateTotal()
    clear()
}

These methods naturally belong together.


14. Low Cohesion#

Think:

Class
 ├── payment
 ├── email
 ├── database
 ├── logging
 ├── image processing
 └── random utilities

This class becomes difficult to understand.


15. Coupling#

Coupling describes how strongly one component depends on another.

Example:

Java
class OrderService {

    private final StripePayment payment;

    OrderService() {
        payment =
            new StripePayment();
    }
}

OrderService is tightly connected to a specific payment implementation.


16. Lower Coupling#

Use an abstraction:

Java
interface Payment {

    void pay(double amount);
}

Then:

Java
class OrderService {

    private final Payment payment;

    OrderService(Payment payment) {
        this.payment = payment;
    }
}

Now OrderService does not need to know the concrete implementation.


17. Coupling vs Cohesion#

Remember this simple comparison:

Output
Coupling
→ relationship BETWEEN classes

Cohesion
→ relationship WITHIN a class

Good design often aims for:

Output
LOW unnecessary coupling
HIGH cohesion

18. Why Low Coupling Helps#

Suppose:

Output
OrderService → Payment

If Payment is an interface:

Diagram
OrderService → Payment
                    ↑
             ┌──────┴──────┐
             │             │
        CardPayment    UpiPayment

You can replace implementations without changing the OrderService's core logic.


19. Programming to an Abstraction#

Instead of:

Java
CardPayment payment;

prefer:

Java
Payment payment;

when the code only needs Payment behavior.

Example:

Java
interface Payment {

    void pay(double amount);
}

class Checkout {

    private final Payment payment;

    Checkout(Payment payment) {
        this.payment = payment;
    }
}

This is programming to an abstraction.


20. Why Abstractions Help#

The class says:

Output
"I need something that can make a payment."

rather than:

Output
"I specifically need this exact payment class."

This gives the design flexibility.


21. Dependency Injection#

Dependency injection means supplying a dependency from outside.

Example:

Java
class Checkout {

    private final Payment payment;

    Checkout(Payment payment) {
        this.payment = payment;
    }
}

The Checkout object does not create its own Payment implementation.

The caller supplies it.


22. Why Dependency Injection Helps#

It can improve:

Output
testability
replaceability
flexibility
separation of concerns

For example:

Java
Checkout real =
    new Checkout(
        new CardPayment()
    );

and for testing:

Java
Checkout test =
    new Checkout(
        new FakePayment()
    );

23. Open/Closed Principle#

The second SOLID principle:

Output
O — Open/Closed Principle

A common explanation:

Software entities should be open for extension but closed for modification.

In simple language:

Output
Add new behavior
without repeatedly changing stable existing code.

24. Example — Payment#

Suppose you have:

Java
interface Payment {

    void pay(double amount);
}

Implementations:

Java
class CardPayment
        implements Payment {

    public void pay(double amount) {
        System.out.println(
            "Card payment"
        );
    }
}
Java
class UpiPayment
        implements Payment {

    public void pay(double amount) {
        System.out.println(
            "UPI payment"
        );
    }
}

Checkout can depend on Payment.


25. Extending Payment#

Later:

Java
class WalletPayment
        implements Payment {

    public void pay(double amount) {
        System.out.println(
            "Wallet payment"
        );
    }
}

If Checkout already works with Payment, it may not need modification.

You extended the system with a new implementation.


26. OCP Does Not Mean "Never Modify Code"#

This principle is not absolute.

Real software must sometimes be changed.

OCP means that a well-designed extension point can allow new variations without repeatedly modifying stable code.

Do not interpret it as:

Output
"Existing code can never be edited."

27. Liskov Substitution Principle#

The third SOLID principle:

Output
L — Liskov Substitution Principle

The core idea:

A subtype should be usable wherever its parent type is expected without breaking the expected behavior.

This is deeply connected to polymorphism.


28. Simple LSP Example#

Java
class Bird {

    void eat() {
        System.out.println(
            "Eating"
        );
    }
}

class Sparrow
        extends Bird {

    void fly() {
        System.out.println(
            "Flying"
        );
    }
}

A Sparrow can be used where Bird is expected:

Java
Bird bird =
    new Sparrow();

bird.eat();

No problem.


29. Classic LSP Warning — Rectangle/Square#

A common teaching example is:

Rectangle
    ↑
  Square

Mathematically, a square is a rectangle.

But if a Rectangle API allows:

Java
setWidth()
setHeight()

and assumes width and height can change independently, making Square a subtype can violate those behavioral expectations.

The problem is not mathematics.

The problem is whether the subtype preserves the assumptions made by users of the parent type.


30. LSP Is About Behavior#

Inheritance is not correct simply because:

Output
"This thing is technically a type of that thing."

Ask:

Output
Can the child safely replace the parent?

If not, inheritance may be the wrong design.


31. LSP and Method Overriding#

Suppose:

Java
class Bird {

    void move() {
        System.out.println(
            "Moving"
        );
    }
}

A subtype should not unexpectedly break the contract of move().

If a parent method promises:

Output
"move performs a valid movement"

the child should preserve that expectation.


32. LSP and Exceptions#

Suppose a parent method is expected to succeed under certain valid conditions.

A child should not introduce surprising failures that violate the parent's contract.

LSP is about preserving the behavioral expectations of the abstraction.

This becomes especially important in large inheritance hierarchies.


33. Interface Segregation Principle#

The fourth SOLID principle:

Output
I — Interface Segregation Principle

A common explanation:

Clients should not be forced to depend on methods they do not need.

In simple language:

Output
Prefer small, focused interfaces
over one huge interface.

34. Bad Interface#

Java
interface Machine {

    void print();

    void scan();

    void fax();
}

Suppose a simple printer only supports printing.

Forcing it to implement:

Output
scan()
fax()

is a design problem.


35. Better Interfaces#

Split the responsibilities:

Java
interface Printable {

    void print();
}
Java
interface Scannable {

    void scan();
}
Java
interface Faxable {

    void fax();
}

Now a printer can implement only what it supports.


36. Multiple Interfaces#

Java allows:

Java
class MultiFunctionPrinter
        implements
        Printable,
        Scannable,
        Faxable {

    public void print() {
    }

    public void scan() {
    }

    public void fax() {
    }
}

A simple printer can implement only:

Java
class SimplePrinter
        implements Printable {

    public void print() {
    }
}

This is a good example of interface segregation.


37. Dependency Inversion Principle#

The fifth SOLID principle:

Output
D — Dependency Inversion Principle

A common explanation:

High-level modules should not depend directly on low-level modules. Both should depend on abstractions.

Also:

Abstractions should not depend on details. Details should depend on abstractions.


38. Simple DIP Example#

Bad:

Java
class OrderService {

    private final MySQLDatabase db =
        new MySQLDatabase();
}

OrderService directly depends on a concrete database implementation.


39. Better DIP Example#

Define abstraction:

Java
interface OrderRepository {

    void save(Order order);
}

Implementation:

Java
class MySQLOrderRepository
        implements OrderRepository {

    public void save(Order order) {
        System.out.println(
            "Saving to MySQL"
        );
    }
}

Service:

Java
class OrderService {

    private final OrderRepository repository;

    OrderService(
        OrderRepository repository
    ) {
        this.repository = repository;
    }
}

Now:

OrderService
      ↓
OrderRepository
      ↑
MySQLOrderRepository

40. SOLID Overview#

Remember:

Output
S → Single Responsibility
    One focused reason to change

O → Open/Closed
    Extend behavior without unnecessary modification

L → Liskov Substitution
    Subtypes should preserve parent expectations

I → Interface Segregation
    Prefer focused interfaces

D → Dependency Inversion
    Depend on abstractions, not concrete details

41. SOLID Is Not a Collection of Java Keywords#

There is no:

Java
solid

keyword.

SOLID is a set of design principles.

Java features that help implement these ideas include:

Output
classes
interfaces
abstract classes
inheritance
composition
polymorphism
access modifiers
dependency injection

42. Composition Over Inheritance#

You learned this in Chapter 22.

Now connect it to design.

Suppose:

Output
Car
Engine

Do not use:

Java
class Car extends Engine {
}

because Car is not Engine.

Use:

Java
class Car {

    private final Engine engine;

    Car(Engine engine) {
        this.engine = engine;
    }
}

43. Why Composition Is Often More Flexible#

Inheritance creates a fixed type hierarchy.

Composition allows objects to collaborate.

Example:

Car
 ├── Engine
 ├── BrakeSystem
 ├── Navigation
 └── AudioSystem

Components can often be replaced independently.


44. Deep Inheritance Is a Warning Sign#

Consider:

Animal
  ↓
Mammal
  ↓
DomesticMammal
  ↓
Pet
  ↓
SpecialDog

Deep hierarchies can create:

Output
hard-to-follow behavior
fragile parent-child dependencies
unexpected inherited behavior
difficult changes

Inheritance is useful, but do not create deep hierarchies without a strong reason.


45. Immutable Objects#

An immutable object is an object whose observable state does not change after construction.

Examples from Java include:

Output
String

and many value-like classes designed to be immutable.

A typical immutable class:

Java
final class Person {

    private final String name;
    private final int age;

    Person(
        String name,
        int age
    ) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

There are no setters that change the state.


46. Basic Rules for Immutability#

A common design approach:

Output
1. Prevent unintended subclass mutation.
2. Keep state private.
3. Make fields final where appropriate.
4. Initialize state during construction.
5. Do not provide mutating methods.
6. Defensively copy mutable input/output.

The exact rules depend on the fields and class design.


47. Why Immutability Helps#

Immutable objects can make code easier to reason about because their state does not unexpectedly change.

Benefits can include:

Output
safer sharing
simpler reasoning
fewer accidental state changes
easier testing
better suitability for concurrent use

Immutability does not automatically make every class thread-safe, but immutable state removes many common mutation problems.


48. final Field Does Not Automatically Make an Object Immutable#

Important:

Java
private final List<String> names;

The reference cannot be reassigned.

But the List itself may still be mutable:

Java
names.add("Aman");

So:

Output
final reference
≠
immutable object

49. Defensive Copying#

Suppose:

Java
class Student {

    private final List<String> subjects;

    Student(List<String> subjects) {
        this.subjects = subjects;
    }
}

The caller still owns the original List.

They can modify it later.

A defensive copy can help:

Java
this.subjects =
    new ArrayList<>(subjects);

Now Student has its own list.


50. Returning Defensive Copies#

Suppose:

Java
List<String> getSubjects() {
    return subjects;
}

The caller may modify the internal list.

Safer options can include:

Java
return List.copyOf(subjects);

or:

Java
return Collections.unmodifiableList(subjects);

These have different semantics.


51. copyOf vs unmodifiableList#

Consider:

Java
return List.copyOf(subjects);

This returns an unmodifiable copy.

Later changes to the original list are not reflected in that returned copy.

Compare:

Java
return Collections.unmodifiableList(subjects);

This returns an unmodifiable view.

Changes to the underlying list can still be visible through the view.


52. Immutable Value Objects#

A value object represents a value rather than an entity whose identity is the main concern.

Examples:

Output
Money
EmailAddress
Coordinate
DateRange
Address

For example:

Java
final class Money {

    private final int amount;
    private final String currency;

    Money(
        int amount,
        String currency
    ) {
        this.amount = amount;
        this.currency = currency;
    }

    int getAmount() {
        return amount;
    }

    String getCurrency() {
        return currency;
    }
}

53. Value Equality#

Value objects often use logical equality.

For example:

Output
Money(100, "INR")

and another:

Output
Money(100, "INR")

may represent the same value.

Such a class should normally implement equals() and hashCode() consistently.

This connects directly to Chapter 10's discussion of String equality and Java's Object methods.


54. Entity vs Value#

An entity often has identity:

Output
Employee ID = 101

A value object represents a value:

Output
Money = ₹500

Two objects may have:

Output
different identities
same value

This distinction is useful in domain modeling.


55. Behavior-Rich Objects#

OOP is not just:

Output
fields
+
getters
+
setters

Good objects often keep related behavior close to their data.

Example:

Java
class BankAccount {

    private double balance;

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

        balance += amount;
    }

    void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Amount must be positive"
            );
        }

        if (amount > balance) {
            throw new IllegalArgumentException(
                "Insufficient balance"
            );
        }

        balance -= amount;
    }

    double getBalance() {
        return balance;
    }
}

The object controls its own rules.


56. Anemic Object#

An anemic model often has:

Output
private fields
+
getters
+
setters

while business rules live elsewhere.

Example:

Java
class BankAccount {

    private double balance;

    double getBalance() {
        return balance;
    }

    void setBalance(double balance) {
        this.balance = balance;
    }
}

Then another class performs:

Java
account.setBalance(
    account.getBalance() - amount
);

This can weaken encapsulation.


57. Why Setters Can Be Dangerous#

Suppose:

Java
account.setBalance(-5000);

If negative balances are invalid, the setter exposes too much control.

Better:

Java
account.withdraw(5000);

The Account class can enforce its own rules.

This is encapsulation plus behavior-rich design.


58. Tell, Don't Ask#

A useful design principle:

Tell an object what to do instead of asking for its internal data and doing the work yourself.

Instead of:

Java
if (account.getBalance() >= amount) {
    account.setBalance(
        account.getBalance() - amount
    );
}

prefer:

Java
account.withdraw(amount);

The Account owns the withdrawal rules.


59. Why Tell, Don't Ask Helps#

It keeps business rules near the data they protect.

Instead of:

Output
Service knows Account internals

we get:

Output
Service asks Account to withdraw
Account enforces Account rules

This usually improves encapsulation.


60. Law of Demeter — Basic Idea#

The Law of Demeter is a design guideline about limiting unnecessary knowledge of object structures.

A common warning sign is long chains:

Java
order.getCustomer()
     .getAddress()
     .getCity()
     .getName();

Such chains can create coupling to internal object structures.


61. Why Long Chains Can Hurt#

If the structure changes:

Output
Order
 → Customer
 → Address
 → City

many callers may need modification.

Sometimes a better API is:

Java
order.getShippingCity();

where Order provides the meaningful operation or value needed by the caller.

Do not blindly ban all method chaining; the principle is about unnecessary structural coupling.


62. God Class#

A God class is a class that knows or does far too much.

Example:

Java
class ApplicationManager {

    // users
    // payments
    // database
    // email
    // reports
    // authentication
    // logging
    // file handling
    // UI
    // configuration
}

This class becomes a central point of complexity.


63. Problems with God Classes#

They tend to have:

Output
low cohesion
high coupling
large size
many reasons to change
difficult testing
difficult reuse

Break the class into meaningful responsibilities.


64. Example — Refactoring a God Class#

Before:

Java
class Shop {

    void registerUser() {}
    void loginUser() {}
    void saveUser() {}
    void processPayment() {}
    void sendEmail() {}
    void generateInvoice() {}
}

Possible separation:

Output
UserService
AuthenticationService
UserRepository
PaymentService
EmailService
InvoiceService

The exact split depends on the application's domain.


65. Avoid Premature Abstraction#

A common beginner mistake is creating interfaces for everything.

Example:

Output
UserServiceInterface
UserServiceInterfaceImpl

when there is only one implementation and no meaningful abstraction boundary.

An interface can be useful, but do not create one merely because "SOLID says interfaces are good."


66. Abstraction Should Have a Purpose#

Create an abstraction when it helps with things such as:

Output
multiple implementations
decoupling
polymorphism
clear contract
testing
architectural boundaries

Not simply:

Output
"Every class must have an interface."

67. Avoid Overengineering#

Bad design can come from too little abstraction.

But it can also come from too much abstraction.

Example:

Output
PaymentManagerFactoryProviderAdapter

for a simple program that only needs:

Java
payment.pay();

Complexity should solve a real problem.


68. YAGNI#

A useful software-development principle:

You Aren't Gonna Need It.

Do not build complicated infrastructure for hypothetical future requirements unless there is a good reason.

Example:

Output
Current requirement:
One payment method.

Do not automatically build:

Output
12 interfaces
7 factories
4 abstract factories
3 adapters

unless the actual design requires them.


69. DRY#

DRY means:

Output
Don't Repeat Yourself

The goal is to avoid unnecessary duplication of knowledge or logic.

Example:

Bad:

Java
double calculateTaxForOrder(...) {
    // tax formula
}

double calculateTaxForInvoice(...) {
    // same tax formula
}

If the same business rule truly needs to stay consistent, consider centralizing it.

But do not blindly combine code merely because two snippets look similar.


70. Duplication vs Accidental Similarity#

Two methods may currently look similar but represent different concepts.

Do not immediately extract them into one abstraction.

Ask:

Output
Do they represent the same business rule?
Will they change for the same reason?

Good abstraction is based on meaning, not just matching lines of code.


71. KISS#

KISS commonly means:

Output
Keep It Simple

Prefer the simplest design that correctly solves the problem.

Simple:

Java
class Calculator {

    int add(int a, int b) {
        return a + b;
    }
}

Do not build a complex architecture when a small class is enough.


72. Design Smells#

A design smell is a warning sign.

Common OOP design smells include:

Output
God class
deep inheritance
large methods
too many parameters
too many responsibilities
tight coupling
low cohesion
duplicate logic
excessive getters/setters
public mutable state
unnecessary interfaces
unnecessary abstraction
long method chains

A smell is not always a bug.

It tells you:

Output
"Look more closely."

73. Too Many Parameters#

Example:

Java
createUser(
    String name,
    int age,
    String city,
    String state,
    String country,
    String phone,
    String email
);

A better model might introduce:

Output
Address
ContactInfo
User

For example:

Java
createUser(
    String name,
    int age,
    Address address,
    ContactInfo contact
);

This can improve readability and cohesion.


74. Parameter Object#

A group of related parameters can be represented by an object.

Example:

Java
class Address {

    private final String city;
    private final String state;
    private final String country;

    Address(
        String city,
        String state,
        String country
    ) {
        this.city = city;
        this.state = state;
        this.country = country;
    }
}

Then:

Java
void register(
    String name,
    Address address
) {
}

This is easier to understand.


75. Mutable State#

Mutable state can create bugs when many parts of the application can change it.

Bad:

Java
class User {

    public String name;
}

Any code can modify it.

Better:

Java
class User {

    private String name;

    public String getName() {
        return name;
    }

    public void rename(String name) {
        // validation
        this.name = name;
    }
}

The class controls its state.


76. Invariants#

An invariant is a rule that should always remain true for an object.

Example BankAccount:

Output
balance cannot be negative

Example Student:

Output
age must be valid

Example Order:

Output
total cannot be negative

Good encapsulation protects invariants.


77. Constructor and Invariants#

If an object must always be valid, validate during construction.

Java
class Product {

    private final String name;
    private final double price;

    Product(
        String name,
        double price
    ) {

        if (name == null ||
            name.isBlank()) {

            throw new IllegalArgumentException(
                "Invalid name"
            );
        }

        if (price < 0) {

            throw new IllegalArgumentException(
                "Invalid price"
            );
        }

        this.name = name;
        this.price = price;
    }
}

Now every successfully constructed Product satisfies its basic rules.


78. Make Invalid States Hard to Represent#

This is a powerful design idea.

Instead of:

Java
Order order =
    new Order();

order.setStatus("xyz");

use:

Java
order.cancel();
order.ship();
order.deliver();

The class can control valid state transitions.


79. State Machine Thinking#

An Order might have:

CREATED
   ↓
PAID
   ↓
SHIPPED
   ↓
DELIVERED

Invalid transitions should be rejected.

Good object design can encode such rules.


80. Example — Order State#

Java
enum OrderStatus {

    CREATED,
    PAID,
    SHIPPED,
    DELIVERED,
    CANCELLED
}

Then:

Java
class Order {

    private OrderStatus status =
        OrderStatus.CREATED;

    void pay() {
        if (status != OrderStatus.CREATED) {
            throw new IllegalStateException(
                "Order cannot be paid now"
            );
        }

        status = OrderStatus.PAID;
    }
}

The object controls the state transition.


81. Favor Explicit Behavior#

Compare:

Java
order.setStatus(OrderStatus.SHIPPED);

with:

Java
order.ship();

The second communicates intent better.

It also lets the Order validate whether shipping is allowed.


82. Good Class API#

A good public API should expose:

Output
what callers need

and hide:

Output
how the class internally works

Example:

Java
account.withdraw(1000);

Caller does not need to know:

Output
how balance is stored
how validation works
how transaction records are created

83. Hide Implementation Details#

Suppose internally:

Java
private double balance;

Today.

Tomorrow you might change to:

Java
private BigDecimal balance;

If callers only use:

Java
deposit()
withdraw()
getBalance()

the internal implementation can change with fewer effects.

This is the value of encapsulation.


84. Avoid Public Fields#

Prefer:

Java
private final String name;

instead of:

Java
public String name;

Public fields expose implementation details and make validation and future changes harder.


85. Getters Are Not Always Automatically Good#

Beginners often think:

Output
private field
+
getter
+
setter
=
perfect encapsulation

Not always.

If you expose every internal detail through getters, callers can become dependent on the class's internal structure.

Expose meaningful operations where possible.


86. Example#

Instead of:

Java
cart.getItems().add(product);

prefer:

Java
cart.addItem(product);

Why?

Because Cart can control:

Output
null checks
duplicates
quantity
inventory rules
pricing
events

The caller does not manipulate internal state directly.


87. Collections and Encapsulation#

Bad:

Java
class Cart {

    private final List<Product> items =
        new ArrayList<>();

    List<Product> getItems() {
        return items;
    }
}

Caller can:

Java
cart.getItems().clear();

without Cart knowing why.

Better:

Java
void addItem(Product product) {
    items.add(product);
}

void removeItem(Product product) {
    items.remove(product);
}

88. Defensive Encapsulation#

If callers truly need read access:

Java
List<Product> getItems() {
    return List.copyOf(items);
}

This allows observation without allowing the caller to modify the internal list through the returned reference.


89. Class Size#

There is no universal rule like:

Output
class must be under 100 lines

or:

Output
class must have 5 methods

Instead ask:

Output
Is the class understandable?
Does it have a focused responsibility?
Are its methods related?
Is it changing for many unrelated reasons?

90. Method Size#

Likewise, there is no magic maximum line count.

A long method may be a warning if it:

Output
does many unrelated tasks
contains deeply nested logic
is hard to test
has many local variables
has many branches

Extract meaningful methods based on responsibilities.


91. Naming Is Part of Design#

Good:

Java
calculateTotal()
withdraw()
cancel()
sendInvoice()

Poor:

Java
doStuff()
process()
handle()
run()

unless the generic name is genuinely appropriate.

Names communicate the model.


92. Domain Language#

Use names from the problem domain.

If the application is a library:

Output
Book
Member
Loan
Fine

If it is banking:

Output
Account
Transaction
Deposit
Withdrawal

Good domain names make the code easier to understand.


93. Avoid Boolean Parameter Confusion#

Example:

Java
createUser(
    "Aman",
    true,
    false,
    true
);

What do these booleans mean?

It is difficult to understand.

Better:

Java
UserOptions options =
    new UserOptions(
        true,
        false,
        true
    );

or use meaningful methods/options.


94. Tell the Story Through Code#

Compare:

Java
if (order.getStatus() == PAID) {
    order.setStatus(SHIPPED);
}

with:

Java
order.ship();

The second expresses the domain action.

The object owns the rule.


95. Dependency Direction#

A healthy design often has dependencies flowing toward stable abstractions.

Example:

Application
     ↓
Payment
     ↑
CardPayment

The high-level application depends on the Payment contract.

The concrete implementation depends on that contract by implementing it.


96. Dependency Inversion vs Dependency Injection#

Do not confuse them.

Dependency Inversion:

Output
design principle

Dependency Injection:

Output
technique for supplying dependencies

Example:

Java
Checkout(Payment payment)

is dependency injection.

Using:

Java
Payment

instead of a concrete implementation can support dependency inversion.


97. Interface vs Abstract Class in Design#

Use an interface when you mainly need:

Output
contract
capability
multiple implementations
multiple inheritance of type

Use an abstract class when you need:

Output
shared state
shared implementation
constructor
common protected behavior

The exact choice depends on the design.


98. Composition + Interface#

One of the strongest combinations:

Java
class Checkout {

    private final Payment payment;

    Checkout(Payment payment) {
        this.payment = payment;
    }
}

This gives:

Output
composition
+
abstraction
+
polymorphism
+
dependency injection

This pattern appears frequently in real applications.


99. Testing and Good Design#

Good design often makes testing easier.

Suppose:

Java
class OrderService {

    private final Payment payment;

    OrderService(Payment payment) {
        this.payment = payment;
    }
}

During testing, supply:

Java
FakePayment

instead of a real payment gateway.

This avoids real external operations.


100. Fake Dependency Example#

Java
interface Payment {

    void pay(double amount);
}

class FakePayment
        implements Payment {

    double lastAmount;

    @Override
    public void pay(double amount) {
        lastAmount = amount;
    }
}

Test:

Java
FakePayment fake =
    new FakePayment();

OrderService service =
    new OrderService(fake);

Now the service can be tested without a real payment system.


101. Refactoring#

Refactoring means changing the internal structure of code without intentionally changing its externally observable behavior.

Examples:

Output
extract class
extract method
rename method
replace inheritance with composition
introduce interface
move responsibility
encapsulate field
remove duplication

102. Refactoring Example#

Before:

Java
class Order {

    void checkout() {

        // calculate total
        // validate payment
        // save order
        // send email
    }
}

After:

Output
Order
OrderCalculator
PaymentService
OrderRepository
EmailService

The system may now be easier to maintain.


103. Refactoring Should Be Incremental#

Do not rewrite an entire project blindly.

A safer approach:

Output
1. Understand current behavior.
2. Identify one design problem.
3. Make one small change.
4. Compile/test.
5. Repeat.

This reduces the chance of introducing unrelated bugs.


104. OOP Design Example — Bad#

Java
class ShoppingApplication {

    private List<Product> products;

    void addProduct(Product product) {
        products.add(product);
    }

    void processPayment(double amount) {
        // payment gateway
    }

    void sendEmail(String message) {
        // email
    }

    void saveDatabase() {
        // database
    }

    void generateReport() {
        // reporting
    }

    void calculateTax() {
        // tax
    }
}

Problems:

Output
low cohesion
high coupling
many reasons to change
too many responsibilities
hard testing

105. OOP Design Example — Better#

Split responsibilities:

Java
class ShoppingCart {

    void addProduct(Product product) {
    }

    double calculateTotal() {
        return 0;
    }
}
Java
class PaymentService {

    void pay(double amount) {
    }
}
Java
class EmailService {

    void send(String message) {
    }
}
Java
class ProductRepository {

    void save(Product product) {
    }
}

Now each class has a clearer purpose.


106. Practical Program — High Cohesion#

Java
class BankAccount {

    private final String accountNumber;
    private double balance;

    BankAccount(
        String accountNumber,
        double openingBalance
    ) {

        if (openingBalance < 0) {
            throw new IllegalArgumentException(
                "Opening balance cannot be negative"
            );
        }

        this.accountNumber =
            accountNumber;

        this.balance =
            openingBalance;
    }

    void deposit(double amount) {

        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Amount must be positive"
            );
        }

        balance += amount;
    }

    void withdraw(double amount) {

        if (amount <= 0) {
            throw new IllegalArgumentException(
                "Amount must be positive"
            );
        }

        if (amount > balance) {
            throw new IllegalArgumentException(
                "Insufficient balance"
            );
        }

        balance -= amount;
    }

    double getBalance() {
        return balance;
    }
}

This class has strong cohesion.

It manages:

Output
account state
deposit
withdrawal
balance rules

107. Practical Program — Open/Closed#

Java
interface Discount {

    double calculate(double amount);
}

class NoDiscount
        implements Discount {

    public double calculate(
        double amount
    ) {
        return 0;
    }
}

class StudentDiscount
        implements Discount {

    public double calculate(
        double amount
    ) {
        return amount * 0.10;
    }
}

class Checkout {

    private final Discount discount;

    Checkout(Discount discount) {
        this.discount = discount;
    }

    double finalPrice(double amount) {

        return amount -
               discount.calculate(amount);
    }
}

New discount types can be added as new implementations.


108. Practical Program — Interface Segregation#

Java
interface Printable {

    void print();
}

interface Scannable {

    void scan();
}

class SimplePrinter
        implements Printable {

    @Override
    public void print() {
        System.out.println(
            "Printing"
        );
    }
}

class OfficeMachine
        implements Printable,
        Scannable {

    @Override
    public void print() {
        System.out.println(
            "Printing"
        );
    }

    @Override
    public void scan() {
        System.out.println(
            "Scanning"
        );
    }
}

Each implementation supports only the capabilities it needs.


109. Practical Program — Dependency Inversion#

Java
interface UserRepository {

    void save(String username);
}

class DatabaseUserRepository
        implements UserRepository {

    @Override
    public void save(
        String username
    ) {
        System.out.println(
            "Saving " +
            username +
            " to database"
        );
    }
}

class UserService {

    private final UserRepository repository;

    UserService(
        UserRepository repository
    ) {
        this.repository = repository;
    }

    void register(String username) {
        repository.save(username);
    }
}

public class Main {

    public static void main(
        String[] args
    ) {

        UserRepository repository =
            new DatabaseUserRepository();

        UserService service =
            new UserService(repository);

        service.register("Aman");
    }
}

Output:

Output
Saving Aman to database

110. Practical Program — Immutable Object#

Java
final class Student {

    private final String name;
    private final int age;

    Student(
        String name,
        int age
    ) {

        if (name == null ||
            name.isBlank()) {

            throw new IllegalArgumentException(
                "Invalid name"
            );
        }

        if (age < 0) {
            throw new IllegalArgumentException(
                "Invalid age"
            );
        }

        this.name = name;
        this.age = age;
    }

    String getName() {
        return name;
    }

    int getAge() {
        return age;
    }
}

Once constructed:

Output
name cannot be changed
age cannot be changed

assuming the fields themselves refer to immutable values.


111. Practical Program — Defensive Copy#

Java
import java.util.ArrayList;
import java.util.List;

final class Student {

    private final List<String> subjects;

    Student(List<String> subjects) {

        this.subjects =
            new ArrayList<>(subjects);
    }

    List<String> getSubjects() {
        return List.copyOf(subjects);
    }
}

The internal collection is protected from direct external mutation.


112. Practical Program — Behavior-Rich Order#

Java
enum OrderStatus {

    CREATED,
    PAID,
    SHIPPED,
    CANCELLED
}

class Order {

    private OrderStatus status =
        OrderStatus.CREATED;

    void pay() {

        if (status !=
            OrderStatus.CREATED) {

            throw new IllegalStateException(
                "Cannot pay this order"
            );
        }

        status = OrderStatus.PAID;
    }

    void ship() {

        if (status !=
            OrderStatus.PAID) {

            throw new IllegalStateException(
                "Only paid orders can ship"
            );
        }

        status = OrderStatus.SHIPPED;
    }

    void cancel() {

        if (status ==
            OrderStatus.SHIPPED) {

            throw new IllegalStateException(
                "Shipped order cannot be cancelled"
            );
        }

        status = OrderStatus.CANCELLED;
    }

    OrderStatus getStatus() {
        return status;
    }
}

The Order object controls its own valid state transitions.


113. Practical Program — Composition Instead of Inheritance#

Java
interface NotificationSender {

    void send(String message);
}

class EmailSender
        implements NotificationSender {

    public void send(
        String message
    ) {
        System.out.println(
            "Email: " + message
        );
    }
}

class SmsSender
        implements NotificationSender {

    public void send(
        String message
    ) {
        System.out.println(
            "SMS: " + message
        );
    }
}

class NotificationService {

    private final NotificationSender sender;

    NotificationService(
        NotificationSender sender
    ) {
        this.sender = sender;
    }

    void notifyUser(String message) {
        sender.send(message);
    }
}

Usage:

Java
NotificationService email =
    new NotificationService(
        new EmailSender()
    );

NotificationService sms =
    new NotificationService(
        new SmsSender()
    );

The service behavior is assembled through composition.


114. Practical Program — Avoiding a God Class#

Instead of:

Java
class ECommerceApplication {
    // everything
}

use:

Java
class ProductService {
}

class CartService {
}

class OrderService {
}

class PaymentService {
}

class NotificationService {
}

Then connect them through clear dependencies.


115. Output Question 1 — Immutability#

Java
final class User {

    private final String name;

    User(String name) {
        this.name = name;
    }

    String getName() {
        return name;
    }
}

Question:

Can this code change the name field after construction?

Answer:

Output
No

The field reference is final and there is no mutating method.


116. Output Question 2 — final Reference#

Java
List<String> list =
    new ArrayList<>();

final List<String> names =
    list;

names.add("Aman");

System.out.println(names);

Output:

Output
[Aman]

Why?

final prevents the reference from being reassigned.

It does not make the List immutable.


117. Output Question 3 — Composition#

Java
interface Engine {

    void start();
}

class PetrolEngine
        implements Engine {

    public void start() {
        System.out.println(
            "Petrol"
        );
    }
}

class Car {

    private final Engine engine;

    Car(Engine engine) {
        this.engine = engine;
    }

    void start() {
        engine.start();
    }
}

new Car(
    new PetrolEngine()
).start();

Output:

Output
Petrol

118. Output Question 4 — Polymorphic Dependency#

Java
interface Payment {

    void pay();
}

class CardPayment
        implements Payment {

    public void pay() {
        System.out.println("Card");
    }
}

class UpiPayment
        implements Payment {

    public void pay() {
        System.out.println("UPI");
    }
}

Payment payment =
    new UpiPayment();

payment.pay();

Output:

Output
UPI

119. Output Question 5 — Behavior#

Java
class Account {

    private double balance = 1000;

    void withdraw(double amount) {

        if (amount > balance) {
            throw new IllegalArgumentException();
        }

        balance -= amount;
    }

    double getBalance() {
        return balance;
    }
}

Account account =
    new Account();

account.withdraw(300);

System.out.println(
    account.getBalance()
);

Output:

Output
700.0

The Account object owns the withdrawal rule.


120. Output Question 6 — Defensive Copy#

Java
List<String> original =
    new ArrayList<>();

original.add("Java");

List<String> copy =
    List.copyOf(original);

original.add("Python");

System.out.println(copy);

Output:

Output
[Java]

The copy does not automatically reflect later changes to the original list.


121. Output Question 7 — Unmodifiable View#

Java
List<String> original =
    new ArrayList<>();

original.add("Java");

List<String> view =
    Collections.unmodifiableList(
        original
    );

original.add("Python");

System.out.println(view);

Output:

Output
[Java, Python]

The view reflects changes to the underlying list, but callers cannot modify the view through its mutation methods.


122. Output Question 8 — SRP#

Java
class Invoice {

    void calculateTotal() {
    }

    void sendEmail() {
    }

    void saveDatabase() {
    }
}

Question:

Does this automatically violate Java syntax?

Answer:

Output
No.

Does it potentially violate good SRP design?

Output
Yes.

It has multiple unrelated responsibilities.


123. Interview Questions — OOP Design Basics#

Q1. What is OOP design?#

OOP design is the process of organizing classes, objects, responsibilities, relationships, and dependencies so that software is understandable, maintainable, testable, and extensible.

Q2. What is cohesion?#

Cohesion describes how closely related the responsibilities inside a class are.

Q3. What is coupling?#

Coupling describes how strongly one component depends on another.

Q4. What is good OOP design?#

A common goal is:

Output
high cohesion
low unnecessary coupling
clear responsibilities
encapsulation
appropriate abstraction
manageable dependencies

Q5. What is SRP?#

A class should have one focused reason to change.


124. Interview Questions — SOLID#

Q6. What does SOLID stand for?#

Output
S — Single Responsibility Principle
O — Open/Closed Principle
L — Liskov Substitution Principle
I — Interface Segregation Principle
D — Dependency Inversion Principle

Q7. Explain OCP.#

Software should be designed so that new behavior can often be added through extension without repeatedly modifying stable existing code.

Q8. Explain LSP.#

Subtypes should be safely usable where their parent type is expected without violating behavioral expectations.

Q9. Explain ISP.#

Clients should not be forced to depend on methods they do not need.

Q10. Explain DIP.#

High-level and low-level modules should depend on abstractions rather than the high-level module directly depending on concrete details.


125. Interview Questions — Immutability#

Q11. What is an immutable object?#

An object whose observable state cannot be changed after construction.

Q12. Is final enough to make a class immutable?#

No.

A final reference can still refer to a mutable object.

Q13. How can you design an immutable class?#

Common techniques:

Output
private state
final fields where appropriate
initialize state in constructor
no mutating setters
defensive copying
final class when subclass mutation must be prevented

Q14. Why is immutability useful?#

It simplifies reasoning about state and can make sharing safer.

Q15. What is defensive copying?#

Creating or returning a copy of mutable data so external code cannot directly modify internal state.


126. Interview Questions — Composition#

Q16. Why favor composition over inheritance?#

Composition can provide more flexibility by assembling behavior from collaborating objects instead of creating rigid inheritance hierarchies.

Q17. Is inheritance bad?#

No.

Use inheritance when there is a genuine subtype relationship and the subtype satisfies the parent contract.

Q18. What is programming to an abstraction?#

Writing code against an interface or abstract contract instead of a concrete implementation.

Q19. What is dependency injection?#

Supplying a dependency from outside the dependent class.

Q20. Is dependency injection the same as dependency inversion?#

No.

Dependency inversion is a design principle.

Dependency injection is a technique for supplying dependencies.


127. Interview Questions — Encapsulation and APIs#

Q21. Why should fields usually be private?#

To control access and protect invariants.

Q22. Are getters and setters always good encapsulation?#

No.

Exposing every internal field through getters/setters can still leak implementation details.

Q23. What is Tell, Don't Ask?#

Prefer asking an object to perform an operation rather than extracting its internal state and performing the operation externally.

Q24. What is a God class?#

A class that contains too many responsibilities and knows or does too much.

Q25. What is high cohesion?#

A class has high cohesion when its responsibilities are strongly related.


128. Interview Questions — Abstraction#

Q26. Should every class have an interface?#

No.

Create abstractions when they provide a meaningful contract, multiple implementations, decoupling, testing value, or another real design benefit.

Q27. What is overengineering?#

Adding unnecessary complexity, abstractions, layers, or patterns that do not solve an actual problem.

Q28. What is YAGNI?#

"You Aren't Gonna Need It."

Avoid building unnecessary features or abstractions only for hypothetical future requirements.

Q29. What is DRY?#

"Don't Repeat Yourself."

Avoid unnecessary duplication of knowledge or logic.

Q30. What is KISS?#

"Keep It Simple."

Prefer simple solutions when they adequately solve the problem.


129. Design Exercise 1 — Bad Payment Design#

Given:

Java
class Checkout {

    void payByCard(double amount) {
    }

    void payByUpi(double amount) {
    }

    void payByWallet(double amount) {
    }
}

Tasks:

Output
1. Identify the design problem.
2. Create a Payment interface.
3. Create CardPayment.
4. Create UpiPayment.
5. Create WalletPayment.
6. Make Checkout depend on Payment.

130. Design Exercise 2 — God Class#

Given:

ApplicationManager
 ├── users
 ├── payments
 ├── emails
 ├── reports
 ├── database
 ├── authentication
 └── logging

Split it into focused classes.

Explain why your design has better cohesion.


131. Design Exercise 3 — Interface Segregation#

Create:

Java
interface SmartDevice {

    void print();
    void scan();
    void fax();
}

Now create:

Output
SimplePrinter
Scanner
FaxMachine
MultiFunctionMachine

Refactor the design using smaller interfaces.


132. Design Exercise 4 — Immutable Student#

Create an immutable:

Output
Student

with:

Output
name
rollNumber
subjects

Requirements:

Output
private fields
final fields
constructor validation
defensive copy of subjects
no setters
safe getter for subjects

133. Design Exercise 5 — Bank Account#

Create:

Output
BankAccount

with:

Output
deposit()
withdraw()
transfer()
getBalance()

Rules:

Output
balance cannot become negative
amount must be positive
account number cannot change

Do not expose a public balance field.


134. Design Exercise 6 — Order State#

Create:

Output
Order

with:

Output
CREATED
PAID
SHIPPED
DELIVERED
CANCELLED

Methods:

Output
pay()
ship()
deliver()
cancel()

Reject invalid state transitions.

Do not provide:

Java
setStatus(...)

135. Design Exercise 7 — Composition#

Create:

Output
NotificationService
NotificationSender
EmailSender
SmsSender
PushSender

NotificationService should use composition and constructor injection.

Do not create:

Output
EmailNotificationService extends NotificationService
SmsNotificationService extends NotificationService
PushNotificationService extends NotificationService

unless inheritance genuinely represents your domain.


136. Design Exercise 8 — Repository Abstraction#

Create:

Output
UserRepository
MySQLUserRepository
MemoryUserRepository
UserService

UserService should depend on UserRepository.

Then create a test using MemoryUserRepository.


137. Design Exercise 9 — Refactoring#

Start with:

Java
class Order {

    void calculateTotal() {}
    void saveToDatabase() {}
    void sendEmail() {}
    void processPayment() {}
}

Refactor it into appropriate classes.

Explain:

Output
SRP
coupling
cohesion
dependency injection

138. Design Exercise 10 — Long Parameter List#

Create:

Java
registerUser(
    String name,
    String city,
    String state,
    String country,
    String phone,
    String email
)

Refactor using:

Output
Address
ContactInfo

Explain why the new design is easier to understand.


139. Design Exercise 11 — Tell, Don't Ask#

Given:

Java
if (account.getBalance() >= amount) {
    account.setBalance(
        account.getBalance() - amount
    );
}

Refactor it to:

Java
account.withdraw(amount);

Move validation and state change into Account.


140. Design Exercise 12 — LSP#

Create a parent abstraction:

Output
Payment

and implementations:

Output
CardPayment
UpiPayment

Make sure every implementation follows the Payment contract.

Then create a deliberately broken implementation and explain why it violates substitutability.


141. Design Exercise 13 — Open/Closed#

Create:

Output
Shape
Circle
Rectangle
Triangle
AreaCalculator

Avoid:

Java
if shape instanceof Circle

for every new shape.

Use polymorphism so each Shape calculates its own area.


142. Design Exercise 14 — Cohesion#

Create a class containing:

Output
calculateSalary()
sendEmail()
saveDatabase()
generateInvoice()
printReport()

Refactor it.

Then explain why each resulting class has higher cohesion.


143. Design Exercise 15 — Coupling#

Create two versions:

Output
Version A:
Service directly creates MySQLRepository

Version B:
Service receives Repository interface

Compare:

Output
coupling
testing
replacement
maintenance

144. Mini Project 1 — Clean E-Commerce Design#

Build:

Output
Customer
Address
Product
Cart
CartItem
Order
Payment
PaymentProcessor
OrderRepository
NotificationSender

Requirements:

Output
1. Customer has Address.
2. Cart contains CartItems.
3. CartItem refers to Product.
4. Order uses Payment.
5. Payment is an interface.
6. NotificationSender is an interface.
7. Repository is an abstraction.
8. Dependencies are injected.
9. Objects protect their own state.
10. Avoid a God class.

145. Mini Project 2 — Banking System#

Build:

Output
Bank
Account
Customer
Transaction
AccountRepository
NotificationService

Requirements:

Output
1. Account controls deposit/withdrawal.
2. Balance cannot be changed directly.
3. Transaction records operations.
4. Repository is an abstraction.
5. Notification is injected.
6. Use immutable value objects where appropriate.
7. Keep classes cohesive.

146. Mini Project 3 — Notification System#

Build:

Output
NotificationService
NotificationSender
EmailSender
SmsSender
PushSender

Requirements:

Output
1. Use an interface.
2. Use composition.
3. Use constructor injection.
4. Support multiple senders.
5. Avoid inheritance for sender selection.
6. Make testing possible using FakeNotificationSender.

147. Mini Project 4 — Library System#

Build:

Output
Library
Book
Member
Loan
FineCalculator
NotificationSender

Requirements:

Output
1. Library manages books.
2. Member can borrow books.
3. Loan represents the borrowing relationship.
4. FineCalculator calculates fines.
5. NotificationSender is an abstraction.
6. Avoid putting every operation in Library.
7. Protect internal collections.

148. Mini Project 5 — Order Processing System#

Build:

Output
Order
OrderItem
Payment
PaymentRepository
OrderRepository
NotificationSender
OrderService

Use:

Output
interfaces
composition
dependency injection
encapsulation
immutable values
SOLID principles

Then explain the design decisions.


149. Challenge — Design Without Inheritance#

Create a game character system.

Characters can have:

Output
movement
attack
defense
weapon

Avoid a large hierarchy such as:

Character
 ↓
Warrior
 ↓
MagicWarrior
 ↓
FireMagicWarrior
 ↓
...

Instead experiment with composition:

Character
 ├── MovementStrategy
 ├── AttackStrategy
 ├── DefenseStrategy
 └── Weapon

150. Challenge — Replace If/Else#

Suppose:

Java
if (type.equals("CARD")) {
    // card
} else if (type.equals("UPI")) {
    // UPI
} else if (type.equals("WALLET")) {
    // wallet
}

Design a polymorphic solution using:

Output
Payment interface
CardPayment
UpiPayment
WalletPayment

151. Challenge — Immutable Money#

Create:

Output
Money

with:

Output
amount
currency

Requirements:

Output
immutable
value equality
validation
add()
subtract()

Example:

Java
Money a =
    new Money(100, "INR");

Money b =
    new Money(50, "INR");

Money c =
    a.add(b);

Expected conceptual result:

Output
150 INR

Do not mutate a or b.


152. Challenge — Valid State Transitions#

Create:

Output
Order

with:

Output
CREATED
PAID
SHIPPED
DELIVERED
CANCELLED

Rules:

Output
CREATED → PAID
PAID → SHIPPED
SHIPPED → DELIVERED
CREATED → CANCELLED
PAID → CANCELLED

Reject invalid transitions.


153. Challenge — Identify Design Smells#

Look at:

Java
class UserManager {

    void createUser() {}
    void deleteUser() {}
    void sendEmail() {}
    void saveToDatabase() {}
    void calculateTax() {}
    void generateInvoice() {}
    void printReport() {}
    void processPayment() {}
}

Identify at least five design problems.

Possible answers:

Output
God class
low cohesion
high coupling
SRP violation
many reasons to change
difficult testing

154. Design Checklist#

When creating a class, ask:

Output
1. What does this class represent?

2. What is its main responsibility?

3. Does it have unrelated responsibilities?

4. What state should it own?

5. What state should it hide?

6. Which invariants must it protect?

7. What behavior belongs inside it?

8. Which other objects does it need?

9. Can those dependencies be abstractions?

10. Should dependencies be injected?

11. Is inheritance actually a true IS-A relationship?

12. Would composition be more flexible?

13. Is the class highly cohesive?

14. Is coupling unnecessarily high?

15. Is the public API exposing too much?

16. Can invalid states be prevented?

17. Is the object mutable unnecessarily?

18. Do collections need defensive copies?

19. Is an interface actually useful?

20. Am I overengineering?

155. The OOP Design Mindset#

A beginner often thinks:

Output
"What classes do I need?"

A better question is:

Output
"What responsibilities exist?"

Then:

Output
Which object should own each responsibility?

Then:

Output
How should those objects collaborate?

Then:

Output
What should be public?
What should remain private?

Then:

Output
What dependencies can change?

This is the beginning of software design.


156. From Classes to Responsibilities#

Do not start with:

Output
I need 20 classes.

Start with:

Output
What does the system need to do?

Example:

Output
Place order
Calculate price
Process payment
Save order
Send confirmation

Then assign responsibilities:

Output
Order
PricingService
Payment
OrderRepository
NotificationSender

This is much more meaningful than inventing classes first.


157. From Responsibilities to Relationships#

Once responsibilities are known:

Output
Order → PricingService
OrderService → Payment
OrderService → OrderRepository
OrderService → NotificationSender

Now determine:

Output
association?
dependency?
composition?
interface?

This connects Chapter 22 with Chapter 23.


158. From Relationships to Abstractions#

If a dependency can vary:

Payment
 ├── CardPayment
 ├── UpiPayment
 └── WalletPayment

Use an abstraction:

Java
interface Payment

If the implementation is stable and variation is not needed, do not automatically create an interface.


159. From Abstractions to Polymorphism#

Now:

Java
Payment payment =
    new CardPayment();

or:

Java
Payment payment =
    new UpiPayment();

The high-level code can work with:

Java
Payment

without caring about the exact implementation.


160. From Polymorphism to Extensibility#

Later:

Java
class WalletPayment
        implements Payment {
}

You can add a new implementation.

This is where:

Output
interfaces
polymorphism
composition
dependency injection
OCP
DIP

come together.


161. Complete Design Example#

Consider:

Output
Online Store

Requirements:

Output
customers place orders
orders contain products
payment can be Card/UPI
orders are stored
confirmation is sent

A reasonable design might be:

Customer
   |
   ↓
Order
   |
   +── OrderItem
   |      |
   |      ↓
   |    Product
   |
   +── Payment
   |
   +── OrderRepository
   |
   +── NotificationSender

Not every line represents the same relationship.

That is why relationship analysis matters.


162. Complete Design Example — Interfaces#

Java
interface Payment {

    void pay(double amount);
}
Java
interface OrderRepository {

    void save(Order order);
}
Java
interface NotificationSender {

    void send(String message);
}

Then:

Java
class OrderService {

    private final Payment payment;
    private final OrderRepository repository;
    private final NotificationSender sender;

    OrderService(
        Payment payment,
        OrderRepository repository,
        NotificationSender sender
    ) {
        this.payment = payment;
        this.repository = repository;
        this.sender = sender;
    }
}

This is a strong example of dependency inversion and dependency injection.


163. Why This Design Is Flexible#

You can replace:

Output
Payment
OrderRepository
NotificationSender

without rewriting the core OrderService.

For example:

Output
CardPayment
UpiPayment

MySQLOrderRepository
MemoryOrderRepository

EmailSender
SmsSender

The service depends on contracts.


164. But Do Not Overuse This Pattern#

For a tiny program:

Java
class Calculator {

    int add(int a, int b) {
        return a + b;
    }
}

you probably do not need:

Output
CalculatorInterface
CalculatorFactory
CalculatorProvider
CalculatorManager
CalculatorAdapter

Good design is not maximum abstraction.

Good design is appropriate abstraction.


165. Architecture Emerges From Good Design#

As systems become larger:

classes
 ↓
components
 ↓
modules
 ↓
services
 ↓
applications

Good class design helps create good larger architecture.

Bad dependencies at the class level can become bad dependencies at the application level.


166. OOP Design and Maintainability#

A maintainable system should make common changes reasonably local.

Example:

Output
Change email provider

Ideally:

Output
EmailSender implementation

changes.

You should not need to rewrite:

Output
Order
Customer
Product
Cart
Payment

This is one practical way to judge design quality.


167. OOP Design and Testing#

Good boundaries allow isolated testing.

For example:

Output
Order
→ test order rules

Payment
→ test payment behavior

PricingService
→ test pricing rules

OrderRepository
→ test persistence separately

Clear responsibilities reduce the amount of setup needed for individual tests.


168. OOP Design and Change#

Ask:

"If this requirement changes, how many classes must I modify?"

Example:

Output
Add a new payment method.

Good design:

Output
create new Payment implementation

Poor design:

Output
modify 10 unrelated classes

This is a practical design test.


169. OOP Design and Debugging#

When responsibilities are clear:

Output
wrong price?
→ PricingService

payment problem?
→ Payment implementation

database problem?
→ Repository

email problem?
→ NotificationSender

When everything is in one class:

Output
everything could be the problem

Good design helps debugging.


170. Common Beginner Mistakes#

Avoid these:

Output
1. Using inheritance only for code reuse.

2. Making every field public.

3. Creating getters and setters for everything.

4. Creating one giant class.

5. Creating an interface for every class automatically.

6. Using deep inheritance unnecessarily.

7. Putting all business logic into Service classes.

8. Letting outside code manipulate internal collections.

9. Allowing invalid object states.

10. Creating too many abstractions.

11. Copy-pasting business rules.

12. Ignoring coupling.

13. Ignoring cohesion.

14. Making dependencies with concrete implementations unnecessarily.

15. Confusing final references with immutable objects.

16. Treating SOLID as rigid laws instead of design guidelines.

17. Designing for imaginary future requirements.

18. Using patterns just to show knowledge.

19. Overusing static mutable state.

20. Optimizing architecture before understanding the problem.

171. SOLID in One Real Example#

Suppose:

Output
OrderService

needs:

Output
Payment
Repository
Notification

Use:

Java
interface Payment {
    void pay(double amount);
}

interface OrderRepository {
    void save(Order order);
}

interface NotificationSender {
    void send(String message);
}

Then:

Java
class OrderService {

    private final Payment payment;
    private final OrderRepository repository;
    private final NotificationSender sender;

    OrderService(
        Payment payment,
        OrderRepository repository,
        NotificationSender sender
    ) {
        this.payment = payment;
        this.repository = repository;
        this.sender = sender;
    }
}

This demonstrates several principles:

Output
SRP
→ OrderService coordinates order processing

OCP
→ new implementations can be added

LSP
→ implementations should honor contracts

ISP
→ focused interfaces

DIP
→ OrderService depends on abstractions

172. SOLID Is About Trade-Offs#

SOLID does not mean:

Output
more classes = better
more interfaces = better
more abstraction = better

Instead:

Output
Use the principle when it improves the design.

A small application may need very little abstraction.

A large application may need carefully designed boundaries.


173. Design Is Context Dependent#

There is rarely one perfect design.

For:

Output
small college assignment

you may use:

Output
5 classes

For:

Output
large production system

you may need:

Output
interfaces
modules
repositories
services
domain objects
dependency injection

Good design depends on:

Output
problem
requirements
team
scale
change frequency
testing needs

174. Final Mental Model#

Think of OOP design like this:

              REQUIREMENTS
                   ↓
             RESPONSIBILITIES
                   ↓
                 CLASSES
                   ↓
                OBJECTS
                   ↓
              RELATIONSHIPS
                   ↓
              ABSTRACTIONS
                   ↓
             DEPENDENCIES
                   ↓
          MAINTAINABLE SOFTWARE

And continuously ask:

Output
Is this class responsible for the right thing?

Is this relationship correct?

Is this dependency necessary?

Can this implementation change independently?

Am I exposing too much?

Am I adding unnecessary complexity?

175. Chapter Summary#

In this chapter, you learned that good OOP is not just about using:

Output
class
object
extends
implements

Good OOP is about design.

The major principles are:

Output
High cohesion
Low unnecessary coupling
Encapsulation
Clear responsibilities
Programming to abstractions
Composition over inheritance
Dependency injection
Immutability where useful

You also learned SOLID:

Output
S → Single Responsibility
O → Open/Closed
L → Liskov Substitution
I → Interface Segregation
D → Dependency Inversion

And several practical design ideas:

Output
Tell, Don't Ask
Law of Demeter
DRY
KISS
YAGNI
Defensive copying
Value objects
Behavior-rich objects
Invariant protection
Avoiding God classes
Avoiding overengineering

176. Final Revision Checklist#

Before moving forward, make sure you can explain:

Output
[ ] What is software design?
[ ] What is good OOP design?
[ ] What is responsibility?
[ ] What is cohesion?
[ ] What is coupling?
[ ] High cohesion vs low cohesion
[ ] High coupling vs low coupling
[ ] Single Responsibility Principle
[ ] Open/Closed Principle
[ ] Liskov Substitution Principle
[ ] Interface Segregation Principle
[ ] Dependency Inversion Principle
[ ] Complete SOLID
[ ] Programming to abstractions
[ ] Dependency Injection
[ ] Composition over inheritance
[ ] Immutable objects
[ ] final vs immutable
[ ] Defensive copying
[ ] Value objects
[ ] Behavior-rich objects
[ ] Anemic object model
[ ] Tell, Don't Ask
[ ] Law of Demeter
[ ] God class
[ ] Overengineering
[ ] YAGNI
[ ] DRY
[ ] KISS
[ ] Invariants
[ ] Encapsulation and API design
[ ] Protecting collections
[ ] Design smells
[ ] Refactoring
[ ] Designing for change
[ ] Designing for testing

177. Final OOP Foundation#

You have now completed the main OOP theory section:

Diagram
Chapter 11
OOP Fundamentals
        ↓
Chapter 12
Classes & Objects
        ↓
Chapter 13
Constructors
        ↓
Chapter 14
this & static
        ↓
Chapter 15
Encapsulation
        ↓
Chapter 16
Inheritance
        ↓
Chapter 17
Method Overloading
        ↓
Chapter 18
Method Overriding
        ↓
Chapter 19
Polymorphism
        ↓
Chapter 20
Abstraction
        ↓
Chapter 21
Interfaces
        ↓
Chapter 22
OOP Relationships
        ↓
Chapter 23
OOP Design

You should now be able to move from:

Output
"I know Java OOP syntax."

to:

Output
"I can reason about how Java classes should be designed."

The next chapters move into the rest of the Java language and standard library:

Output
Chapter 24 → Packages & Access Modifiers
Chapter 25 → Exception Handling
Chapter 26 → File Handling
Chapter 27 → Wrapper Classes
Chapter 28 → Generics
Chapter 29 → Enums
Chapter 30 → Date & Time
Chapter 31 → Collections
...

The OOP concepts from Chapters 11–23 will continue to appear throughout those chapters and especially in the two dedicated OOP projects near the end of the course.