Module 20 · OOP fundamentals · Lesson 1 of 5
Classes and objects
A class describes state and behaviour; an object is one living instance of it with its own identity. Constructors that establish invariants, methods and the receiver, static members, and what an object actually is in memory.
Why it matters
Every object-oriented design, from a three-class interview answer to a large codebase, is built from the same unit: an object that holds some state and offers behaviour that keeps that state valid. If that unit is weak, with state anyone can corrupt and behaviour scattered elsewhere, no pattern on top of it will help. Getting classes and objects right is the foundation the rest of this stage and the whole Low-Level Design track stands on.
The idea
A class is a description: which fields an object of this kind holds, and which methods operate on them. An object is one concrete instance of a class, created at runtime, with its own copy of the fields. Every object has three properties:
| Property | Means | Example for an account |
|---|---|---|
| Identity | This object, distinct from every other, even one with identical values | Account object at a particular memory address |
| State | The current values of its fields | owner = Ana, balance = 120.00 |
| Behaviour | The operations it supports, which read and change its state | deposit, withdraw, balance |
Constructors establish invariants
An invariant is a rule that must always hold for an object to make sense: a balance never negative, an email always well formed, a start date never after an end date. The constructor is the one place every object passes through, so it is where invariants are established; every method afterwards must preserve them. An object that can exist in an invalid state forces every caller to check, which is how bugs spread.
public final class Account {
private final String owner;
private long balanceCents; // invariant: never negative
public Account(String owner, long openingCents) {
if (owner == null || owner.isBlank()) throw new IllegalArgumentException("owner required");
if (openingCents < 0) throw new IllegalArgumentException("opening balance must be >= 0");
this.owner = owner;
this.balanceCents = openingCents;
}
public void withdraw(long cents) {
if (cents <= 0) throw new IllegalArgumentException("amount must be positive");
if (cents > balanceCents) throw new IllegalStateException("insufficient funds");
balanceCents -= cents; // invariant preserved
}
public long balanceCents() { return balanceCents; }
}
Methods and the receiver
A method is a function with an implicit first argument: the object it was called on, called this in Java, C# and TypeScript and self in Python. account.withdraw(500) means "run withdraw with this bound to account". That binding is what lets one method definition serve every instance.
Static members
Static fields and methods belong to the class, not to any object: a counter of accounts created, a factory method, a constant. They are shared by everything, which makes static mutable state effectively a global variable, with the same testing and concurrency problems. Prefer static for constants and pure helpers; keep mutable state in objects.
What an object is in memory
In Java, C#, Python and JavaScript, new allocates the object on the heap and gives you a reference to it. Variables hold references, not objects, so assigning one variable to another copies the reference and both now point at the same object. The runtime adds a small header per object (type information, lock and garbage-collection bits), which is why millions of tiny objects cost more memory than their fields suggest. The Types and memory module goes deeper.
Trade-offs
Rich objects that guard their own invariants make invalid states impossible and keep rules in one place, at the cost of more upfront design and occasional ceremony. Plain data structures with logic elsewhere are quicker to write and fine for data that crosses boundaries (API payloads, database rows), but rules then live in every function that touches the data. Most systems use both: domain objects with behaviour, data transfer objects without.
In an interview
When you introduce a class, say its invariant: "A Booking holds a seat, a show and a status; it can only be confirmed if it is currently held and the hold has not expired, so confirm checks both and is the only way to change status." That one sentence shows you are designing behaviour, not drawing boxes of fields.
Check yourself
In Java, a = new Account('Ana', 100); b = a; b.withdraw(30). What is a.balanceCents()?