OOD Fundamentals
SOLID Principles
Creational Patterns
Structural Patterns
Behavioral Patterns
Classic OOD Problems: Part 1
Classic OOD Problems: Part 2
Classes, Objects, and Encapsulation
Before you can design systems with objects, you need to understand why objects exist in the first place. The fundamental problem is this: real-world software models real-world things, bank accounts, users, shopping carts, sensors, and each of those things has both data (state) and behavior (operations on that state). Without objects, you end up with scattered variables and loose functions that have no clear ownership, making the code fragile and hard to reason about.
A class solves this by acting as a blueprint. It defines what data a thing holds and what operations it supports. An object is a specific instance of that blueprint: a concrete thing living in memory with its own values.

Classes as Blueprints
Think of a class like an architectural blueprint for a house. The blueprint specifies that every house has a number of bedrooms, a square footage, and a color. But the blueprint itself is not a house: you cannot live in it. You must build (instantiate) a house from the blueprint to get something real.
The Dog class defines the shape: every dog has a name, breed, and age, plus behaviors like barking and having birthdays. But no dog exists until you create one.
Objects as Instances
When you instantiate a class, you get an object: a self-contained unit with its own copy of the data.
fido and rex are independent objects. Calling fido.birthday() changes Fido's age but leaves Rex untouched. Each object owns its own state. This independence is the entire point: you can have thousands of Dog objects, each tracking its own data, without any interference.
A common interview mistake is conflating classes with objects. If an interviewer asks you to 'create a User,' they mean define the class. If they say 'create a user,' they mean instantiate an object. The distinction matters because design decisions happen at the class level (what fields and methods exist), while runtime behavior happens at the object level (what values those fields hold).
Why Not Just Use Dictionaries or Structs?
You could store a dog as a dictionary: {"name": "Fido", "breed": "Labrador", "age": 3}. This works for simple cases, but it breaks down quickly. Nothing prevents you from misspelling a key ("naem" instead of "name"), adding unexpected keys, or forgetting to include required fields. There is no place to put behavior: the bark() and birthday() functions float somewhere else, disconnected from the data they operate on.
Classes bind data and behavior together. The constructor enforces that every Dog has a name, breed, and age. The methods live on the class, so you always know where to find them. The type system (in typed languages) catches errors at compile time rather than runtime.
Identity, State, and Behavior
Every object has three defining characteristics:
- Identity, what makes it unique (its memory address or an explicit ID). Two dogs can have the same name and breed but are still different objects.
- State, the current values of its fields. Fido's age is 4 after his birthday.
- Behavior, the operations it supports. Fido can bark and have birthdays.
Understanding this trio helps you design classes that are complete: if an object has state but no meaningful behavior, it might be better as a plain data structure. If it has behavior but no state, it might be better as a module of standalone functions.
Now that you understand what classes and objects are, the next question is: how do you structure the internals of a class? Every class has three building blocks: fields that store state, a constructor that initializes state, and methods that operate on state. Getting these right determines whether your class is easy to use correctly and hard to use incorrectly.
Fields Store State
Fields (also called attributes or instance variables) are the data each object carries. The key design decision is choosing which fields belong on the class. A good rule: a field belongs on a class if it describes an intrinsic property of the entity or is needed by the entity's methods.
Width and height are intrinsic to a rectangle. Color, position on screen, or who created it are extrinsic: they depend on context and usually belong elsewhere.
In Java, fields are declared with explicit types and access modifiers:
The private keyword means no code outside the class can directly read or modify these fields. This is intentional: you control access through methods.
Constructors Enforce Invariants
A constructor is the entry point for creating objects. Its most important job is not just assigning values: it is enforcing invariants. An invariant is a condition that must always be true for the object to be in a valid state.
This constructor guarantees two things: every account has an owner, and no account starts with negative money. If you try to violate either rule, you get an error at creation time: not a mysterious bug later when some method assumes the balance is non-negative.
The constructor is your first line of defense. If an object can never be created in an invalid state, you eliminate an entire category of bugs. This is why experienced developers spend more time designing constructors than any other method: every other method can assume the invariants hold because the constructor guaranteed them.
Methods Define Behavior
Methods are functions that operate on the object's state. Well-designed methods maintain the object's invariants: they never leave the object in an invalid state, no matter what arguments are passed.
Notice the pattern: every method that modifies state validates its input first. deposit() rejects non-positive amounts. withdraw() rejects amounts that would overdraft. The object can never reach a negative balance because both the constructor and every mutating method prevent it.
Instance Methods vs. Class Methods vs. Static Methods
Not every method needs access to an object's state:
Instance methods (self) are the default: they read or modify the object's state. Class methods (cls) are factories that create instances in alternative ways. Static methods are utility functions that logically belong to the class but do not need any state. If a static method does not use any class-specific logic, consider whether it truly belongs on the class or should be a standalone function.
Method Signatures as Contracts
The parameters and return type of a method form a contract with the caller. A good signature tells you what the method needs and what it promises to return: without reading the implementation.
Prefer specific types over generic ones. Prefer descriptive names over abbreviations. A method called withdraw that returns a boolean communicates intent. A method called process that returns an int forces every caller to read the implementation.
You now know how to define classes with fields, constructors, and methods. But knowing how to build a class is different from knowing how to protect it. Encapsulation is the principle that makes your classes robust: it bundles data with the methods that operate on that data, and, critically, it hides the internal details so that outside code cannot tamper with them.
Why does this matter? Because the moment you expose a field directly, every piece of code that touches it becomes a dependency. Change the field's name, type, or validation rules, and you must update every caller. Hide it behind a method, and you can change the internals freely: the method's signature stays the same.

The Problem Encapsulation Solves
Consider a class that exposes its fields directly:
Any code can do this:
The object is now in an invalid state. Every method that assumes a non-negative balance will produce wrong results. The bug is not in the BankAccount class: it is in whoever set the balance directly. But the symptoms appear inside the class, making debugging a nightmare.
Encapsulation prevents this by making the field private and forcing all access through methods that enforce the rules:
Now the only way to change the balance is through deposit() and withdraw(), both of which validate their input. The object can never reach an invalid state: not because callers are disciplined, but because the class makes invalid states impossible.
Interface vs. Implementation
Encapsulation creates a separation between what an object does (its interface) and how it does it (its implementation). The interface is the set of public methods. The implementation is everything else: private fields, internal helper methods, data structures, algorithms.
Callers use add_reading() and average(). They do not know or care that readings are stored in a list. Tomorrow you could switch to a database, a ring buffer, or a running average: as long as add_reading() and average() behave the same way, no caller needs to change.
This is the real power of encapsulation: freedom to evolve the implementation without breaking callers. In large codebases with hundreds of consumers of your class, this freedom is worth everything.
Encapsulation in Different Languages
Languages enforce encapsulation differently:
Java uses access modifiers: private, protected, public, and package-private (default). The compiler enforces these boundaries.
Python uses convention: a single underscore _balance signals "private, do not touch." A double underscore __balance triggers name mangling, making accidental access harder but not impossible. Python trusts developers to follow the convention.
JavaScript (ES2022+) uses the # prefix for truly private fields:
The principle is the same across all languages: separate the public interface from the private implementation. Only the enforcement mechanism differs.
Encapsulation tells you to hide internal state. But how do you decide which parts to expose and which to lock down? And once you make a field private, should callers ever be able to change it at all? Access control and immutability are the tools that answer these questions.
Access Modifiers in Practice
Most object-oriented languages provide a gradient of visibility:

The default should be private. Make a field or method private first. Only widen access when you have a concrete reason. This is the principle of least privilege applied to code.
Notice what is missing: there is no setName(), no getPasswordHash(), no getLoginCount(). Each omission is intentional. The name is immutable after construction. The password hash is never exposed: only compared internally during authentication. The login count is abstracted into a meaningful question (hasLoggedInRecently) rather than leaked as a raw number.
Immutability: Objects That Cannot Change
An immutable object is one whose state cannot be modified after construction. Every field is set in the constructor and never changed.

The translate method does not modify the point: it returns a new Point. The original is untouched. This is the immutability pattern: operations produce new objects instead of modifying existing ones.
Why is this valuable?
Thread safety for free. If an object cannot change, multiple threads can read it simultaneously without locks, mutexes, or synchronization. No thread can corrupt the state because no thread can modify it.
Easier reasoning. When you pass an immutable object to a function, you know it will come back unchanged. No defensive copying needed. No surprises.
Safe hash keys. Mutable objects used as dictionary keys or set elements are dangerous: if you mutate the object, its hash changes, and the dictionary can no longer find it. Immutable objects have stable hashes.
A common pitfall is creating an 'almost immutable' object: all fields are private and there are no setters, but one field is a list that callers can mutate through a getter. If getName() returns a String (immutable), that is safe. But if getItems() returns a List, callers can call getItems().add(x) and modify the internal state. Always return defensive copies of mutable collections, or use immutable collection types.
When to Choose Mutable vs. Immutable
Immutable objects are ideal for value types: things like points, colors, dates, money amounts, and configuration settings. These represent values, not entities. A dollar amount of $5 does not change: you create a new amount.
Mutable objects are appropriate for entity types: things like user accounts, shopping carts, and database connections. These represent things with ongoing lifecycles. A shopping cart adds and removes items over time: creating a new cart on every change would be impractical.
The decision framework:
- Does this object represent a value or an entity?
- Will this object be shared across threads?
- Will this object be used as a dictionary key?
- How frequently does the state change?
If it is a value, shared, used as a key, or rarely changed: make it immutable. If it is an entity with a lifecycle and frequent state changes: make it mutable with careful encapsulation.
You have learned to create classes, protect their state with encapsulation, and choose between mutability and immutability. The final skill is designing the public interface: the set of methods that callers actually use. A clean interface makes your class easy to use correctly and hard to use incorrectly. A messy interface creates confusion, bugs, and frustration.
Tell, Don't Ask
The most common interface mistake is designing classes that expose data and let callers make decisions based on that data. This is called the "ask" pattern: and it scatters logic across the codebase.
In the "ask" pattern, the overdraft logic lives wherever the caller is. If 20 different places withdraw money, the overdraft check is (hopefully) duplicated 20 times. In the "tell" pattern, the logic lives inside withdraw(): one place, one implementation, one set of tests.
The "tell, don't ask" principle says: instead of asking an object for its data and then acting on it, tell the object what you want done and let it figure out how.
Minimize the Public Surface
Every public method is a promise. Once callers depend on it, you cannot remove it, rename it, or change its behavior without breaking things. So expose as few methods as possible.
Notice what is not exposed: the internal _items list. There is no get_items() method. Callers interact with the stack through stack operations (push, pop, peek): not list operations (insert, slice, index). The stack's interface matches the stack abstraction.
Method Naming Conventions
Good method names communicate intent without reading the implementation:
- Verbs for actions:
withdraw(),deposit(),send(),validate() - Questions for queries:
is_empty(),has_permission(),can_withdraw() - Nouns for computed properties:
balance,size,average
Avoid generic names like process(), handle(), do_thing(), or run(). These names force callers to read the implementation to understand what happens. A method named calculate_shipping_cost() tells you exactly what it does. A method named process() tells you nothing.
Cohesion: Every Method Belongs
A class has high cohesion when every method operates on the same set of fields. If a method does not use any of the class's fields, it probably does not belong on the class.
send_email does not use order data: it only uses the customer's email. It belongs on a NotificationService or EmailSender class, not on Order. When a class accumulates unrelated methods, it becomes a grab bag: hard to understand, hard to test, hard to maintain.
Practice Problems
Apply the encapsulation concepts you just learned.
Loading problem...
Loading problem...