AlgoInsight

Free lesson · Low Level Design Fundamentals

What is a Class?

Easy11 min readlldoopdesign-patterns

What Is a Class - Identity, State, Behavior

A class is a blueprint for creating objects. It defines what data each instance holds (state), what operations each instance supports (behavior), and how the system distinguishes one instance from another (identity). These three pillars - identity, state, behavior - are the foundation of object-oriented design. Every design decision in LLD ultimately reduces to questions about how identity, state, and behavior should be distributed across classes.

Understanding them precisely matters in interviews because most LLD problems are exercises in answering exactly that question correctly. Class names are evidence of how the candidate carved up the domain. The interviewer can read your decomposition skill from the classes you propose, before you've written any methods.

Class vs Object - A Critical Distinction

The first thing every senior engineer internalizes: the class is the description; the object (or instance) is the actual entity. One BankAccount class can produce thousands of BankAccount objects. They share the class's structure but each has its own identity, its own state, and its own moment in its lifecycle.

// The class - a blueprint
public class BankAccount {
    private String accountNumber;
    private BigDecimal balance;
    // ...
}

// The objects - actual entities at runtime
BankAccount aliceAccount = new BankAccount("ACC-001", new BigDecimal("100.00"));
BankAccount bobAccount   = new BankAccount("ACC-002", new BigDecimal("250.00"));

aliceAccount and bobAccount are two distinct entities. They share no state. Mutating one doesn't affect the other. They are constructed from the same blueprint but they are not the same thing.

This sounds obvious. But interview answers consistently blur it: "the BankAccount has a balance of

00" is ambiguous - which BankAccount? Senior candidates speak with precision: "this account's balance is
00; that other account has its own balance."

The Three Pillars

graph TD
    A[Class Definition] --> B[Identity<br/>Who am I?]
    A --> C[State<br/>What do I know?]
    A --> D[Behavior<br/>What can I do?]
    B --> B1[Reference equality<br/>Memory address<br/>Domain ID]
    C --> C1[Instance fields<br/>Mutable or immutable<br/>Invariants enforced]
    D --> D1[Public methods<br/>Operations on state<br/>The class contract]
A diagram of class fundamentals showing three pillars: Identity answering "who am I" backed by reference equality, memory address, or domain identifier; State answering "what do I know" backed by instance fields with enforced invariants; Behavior answering "what can I do" backed by public methods that operate on state and define the class contract.

Identity - Who Am I?

Identity is what distinguishes one object from another. Two objects with identical state are still different objects. Two BankAccount instances both holding

00 are not the same account - they have different identities even when their state coincides.

In Java, every object has a built-in identity backed by its memory reference. The == operator compares references; two objects produced by new are never == even if all their fields match. This reference-based identity is automatic - you get it free, regardless of what the class does.

Beyond reference identity, classes often carry a domain identity - an attribute that means "this is the same X" in the business sense. An account number identifies a BankAccount; an email identifies a User in many systems; a UUID identifies almost anything. Domain identity is what survives serialization, persistence, network transfers. Reference identity doesn't.

BankAccount a1 = new BankAccount("ACC-001", balance);
BankAccount a2 = new BankAccount("ACC-001", balance);  // same account number

a1 == a2;                              // false - different objects
a1.accountNumber().equals(a2.accountNumber());  // true - same domain identity

The interview implication: when designing a class, ask which identity matters for this domain. Sometimes reference identity is sufficient (transient objects, in-process caches). Sometimes domain identity is essential (anything that persists or crosses boundaries). The wrong answer to this question produces subtle bugs - duplicate records in the database, broken deduplication, failed reconciliation.

State - What Do I Know?

State is the data the object currently holds. State changes over time in mutable classes; it's fixed at construction in immutable ones (covered in Topic 2.5).

State lives in instance fields - variables declared inside the class but outside any method. Each object has its own copy of every instance field. Modifying aliceAccount.balance doesn't affect bobAccount.balance; they're separate cells in memory.

public class BankAccount {
    private final String accountNumber;   // immutable identity
    private BigDecimal balance;            // mutable state
    private Instant lastModified;          // mutable derived state
    private final List<Transaction> history;  // mutable collection
}

Notice the three categories already visible:

The discipline of senior design: separate these in your head. The class's identity must never mutate. Primary state mutates only through methods that maintain invariants. Derived state must always agree with primary state - otherwise the object is in a corrupt state and downstream code will misbehave.

Behavior - What Can I Do?

Behavior is what the object responds to when called. Methods define the operations. The set of public methods is the class contract - the promises the class makes to its callers.

public class BankAccount {
    // ... fields ...
    
    public void deposit(BigDecimal amount) { /* ... */ }
    public void withdraw(BigDecimal amount) { /* ... */ }
    public BigDecimal balance() { /* ... */ }
    public List<Transaction> recentTransactions(int n) { /* ... */ }
}

A common junior mistake is to treat getters and setters as the "behavior." They aren't - they're state accessors. Real behavior takes inputs, applies domain logic, enforces invariants, mutates state, possibly produces results. deposit(amount) is behavior: it validates, updates balance, updates history, updates timestamp. balance() is a state accessor: it reports without doing anything.

Strong domain models have rich behavior. Weak ones (the Anemic Domain Model anti-pattern, covered in Module 24) are mostly getters and setters with logic scattered across "service" classes. The behavior pillar tells you whether your domain model is doing real work or just holding data.

Anatomy of a Java Class Declaration

A complete class declaration in Java has six syntactic parts. Each part is a design decision:

public final class BankAccount extends Account implements Auditable {
//  ↑      ↑    ↑           ↑               ↑
//  1      2    3           4               5
}
  1. Access modifier - who can use this class? public (everyone), package-private (no modifier; same package), protected (subclasses; for inner classes), private (enclosing class only; for inner classes).

  2. Class modifier - final (cannot be subclassed), abstract (cannot be instantiated; subclass required), sealed (Java 17+; only specific permitted subclasses).

  3. The keyword class - declares this as a class, not an interface, enum, record, or annotation.

  4. Superclass - what does this class inherit from? Default is java.lang.Object. Java permits single inheritance only.

  5. Implemented interfaces - what contracts does this class fulfill? Java permits multiple interface implementation.

Each modifier carries design intent. public final says: "I am a complete leaf class. Use me, but do not extend me." public abstract says: "I am incomplete. You must subclass me to use me." A class declared without final but never subclassed in practice is a missed opportunity to constrain - and protect - the design.

Class vs Instance Members - The static Keyword

A subtle but critical distinction: members of a class can belong to the class itself or to each instance.

public class BankAccount {
    private static int totalAccountsCreated = 0;  // class-level field
    private final String accountNumber;            // instance-level field
    
    public BankAccount(String accountNumber) {
        this.accountNumber = accountNumber;
        totalAccountsCreated++;                    // increments the class state
    }
    
    public static int totalAccounts() {            // class-level method
        return totalAccountsCreated;
    }
    
    public String accountNumber() {                // instance-level method
        return accountNumber;
    }
}

Static (class-level) members exist once for the entire class, regardless of how many instances exist. They are accessed via the class name: BankAccount.totalAccounts(). They cannot access instance fields because they don't belong to any specific instance.

Instance-level members exist once per object. Each BankAccount has its own accountNumber field. They are accessed via an object reference: aliceAccount.accountNumber().

The design question: is this piece of state or behavior about the class as a whole or about a specific instance? Singleton-like data (a counter of all instances) is class-level. Operations on a particular account are instance-level. Mixing the levels - like using static fields to hold per-instance state - is a class-level bug that produces deeply confusing behavior, especially under concurrency.

Where Objects Live - A Memory Model Primer

For senior interviews it helps to have a clear mental model of where Java objects actually exist:

When you write BankAccount account = new BankAccount(...):

  1. The constructor allocates a BankAccount-shaped block on the heap
  2. The constructor runs, initializing fields
  3. A reference to that heap address is stored in the local variable account

When the method returns, account goes out of scope, but the heap object remains until garbage collection determines it's unreachable. This is why holding a reference to an object - in a static collection, for instance - prevents it from being collected. Memory leaks in Java are almost always references held longer than intended.

The interview implication: when an interviewer asks about memory or GC behavior, the reference-vs-object distinction is the starting point. You don't move objects around; you move references to them.

Class Metadata and Reflection

Every Java class has a runtime representation: an instance of java.lang.Class<T>. This metadata enables reflection - inspecting and manipulating classes at runtime.

Class<BankAccount> clazz = BankAccount.class;
String name = clazz.getName();                  // "com.example.BankAccount"
Method[] methods = clazz.getDeclaredMethods();
Field balanceField = clazz.getDeclaredField("balance");

Reflection is rarely used in application code but is the backbone of frameworks: Jackson reflects on classes to deserialize JSON; Spring reflects to wire dependencies; JUnit reflects to find test methods. Understanding that classes themselves are objects (instances of Class) is important when framework behavior surprises you.

Why This Matters in Interviews

Interview: When an interviewer asks "design X," they're asking which classes the system needs. Each class you propose is implicitly making three claims:

  • An identity claim: "Instances of this class are distinguishable entities in the domain. Here's how we tell them apart."
  • A state claim: "These specific pieces of data belong together inside this class because they evolve together and enforce shared invariants."
  • A behavior claim: "These specific operations are this class's responsibility because the data they need lives here."

Strong candidates state each claim explicitly during the design. Weak candidates produce class names without articulating the reasoning. The interviewer scores not the names but the reasoning.

Common interview probes that target the three pillars:

  • "Why is this a class instead of just a function?" - testing whether you have a state claim
  • "How do you tell two of these apart?" - testing the identity claim
  • "What if two callers do this at the same time?" - testing whether the behavior is well-defined
  • "Could this be a value type instead of an entity?" - testing whether identity is genuinely needed

The senior signal is anticipating these probes and articulating the three pillars without being asked.

Common Misconceptions

Warning: Patterns to watch for in your own design:

Classes as namespaces. A class with no instance fields and only static methods isn't really a class - it's a utility module pretending to be a class. Examples: MathUtils, StringHelpers. Sometimes utility classes are fine, but a design dominated by them suggests you've thought in functions rather than entities.

Identity confused with state. Defining "two accounts are equal if they have the same balance" treats coincidental state as identity. Two accounts with the same balance are still different accounts; they will diverge on the next transaction.

State without behavior. A class with only fields and getters/setters is the Anemic Domain Model. The state has nowhere to enforce invariants; behavior gets scattered into "service" classes that operate on the anemic data. Module 24 covers this anti-pattern in detail.

Behavior without state. A class with only methods (no fields) is again a utility module. If the operations don't share state, they don't need to share a class.

Static state confused with instance state. Using static fields to hold per-request data is a classic bug under concurrent or multi-tenant systems. Static state is shared across all callers; what felt like "this user's data" becomes "everyone's data" the moment two users hit the system.

A Worked Example - Designing a Class From the Three Pillars

Suppose an interviewer asks: design a class for a hotel reservation.

public class Reservation {
    // Identity - backed by reservation number (domain identity)
    private final String reservationNumber;
    
    // State - the data this entity holds
    private final Guest primaryGuest;
    private final RoomType roomType;
    private final LocalDate checkIn;
    private final LocalDate checkOut;
    private final Money totalPrice;
    private ReservationStatus status;       // mutable lifecycle state
    
    // Behavior - operations the reservation supports
    public void confirm(PaymentReceipt receipt) { /* ... */ }
    public void cancel(CancellationReason reason) { /* ... */ }
    public void checkIn(LocalDateTime actualTime) { /* ... */ }
    public boolean overlaps(LocalDate start, LocalDate end) { /* ... */ }
    public long nightCount() { /* ... */ }
}

Walking through the three pillars:

Notice what's not in the class: hotel-level concerns like room availability, pricing strategies, guest profiles. Those are other entities or aggregates. The class is disciplined about what's in scope.

This is what senior decomposition looks like. The class has a clear identity, focused state, and behavior that operates on that state. Each pillar is articulated.

Try It

Exercise: For each of these candidate classes, walk through the three pillars:

  1. User in an authentication system
  2. ShoppingCart in an e-commerce system
  3. Timer in a real-time game system
  4. Invoice in an accounting system
  5. RateLimiter in an API gateway

For each, name:

  • The identity (what distinguishes one from another, and is it reference identity, domain identity, or both?)
  • Three pieces of state and whether each is mutable or immutable
  • Three behaviors and what invariants each must maintain

Then for a class you've written recently, audit it: are all three pillars clearly present? If state dominates with no real behavior, you may have an anemic model. If behavior dominates with no state, you may have a utility module pretending to be a class. If identity is unclear, callers may struggle to distinguish instances correctly.

Back to Low Level Design Fundamentals