- Chapter
- Chapter 1 — Object-Oriented Design
- Prerequisites
- Book 1 (Java classes and objects); Volume I Chapter 2 (encapsulation)
- You will be able to
- Describe objects by state, behaviour and identity, design state changes through behaviour, implement identity and equality correctly for entities and value objects, and choose between them
- Covers
- State · Behavior · Identity
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally — Objects on the JVM
- 4A. Deep Dive — Equality Pitfalls
- 5. APIs / Features / Syntax
- 6. Practical Examples
- 7. Common Problems & Mistakes
- 8. Performance & Production Considerations
- 9. Architecture & Design Considerations
- 10. Interview Questions & Answers
1Concept / Definition
An object is a runtime entity that combines state (data), behaviour (operations on that data) and identity (being distinguishable from every other object). Grady Booch's classic definition: an object has state, behaviour, and identity; the structure and behaviour of similar objects are defined in their common class.

| Characteristic | Meaning | Java realisation |
|---|---|---|
| State | The values of an object's properties at a point in time | Fields (ideally private, often final) |
| Behaviour | How an object acts and reacts; operations that read/change state | Methods |
| Identity | The property that distinguishes one object from all others | Object reference (==); domain identity (an ID) for entities |
2Why It Exists
Simula 67 (Dahl and Nygaard) introduced objects to model real-world systems in simulations; Smalltalk (Alan Kay, 1970s) made "objects communicating by messages" the whole programming model. Java (1995) brought objects to mainstream enterprise development. Over time, practice revealed that treating objects as data containers (getters/setters with logic elsewhere) loses most of the benefit; domain-driven design (Evans, 2003) revived the distinction between entities (identity matters) and value objects (only values matter). Java records (16) and pattern matching (21) made value objects and data-oriented modelling first-class.
3Core Concepts
3.1 State
State is the set of values held by an object at a moment. State can be:
| Kind | Description | Example |
|---|---|---|
| Immutable | Fixed at construction | Money, LocalDate, records |
| Mutable, encapsulated | Changes only through the object's methods | Order.status changed by pay() |
| Derived | Computed from other state, not stored | order.total() from lines |
| Transient/technical | Caches, locks, lazy-loaded helpers | @Transient cached hash |

The state space of an object — all combinations of field values — should contain only valid states. Every field adds dimensions; every invariant removes invalid regions.
// Unconstrained state: 2^3 boolean combinations, several invalid
class Shipment { boolean shipped; boolean delivered; boolean cancelled; }
// shipped=false, delivered=true? delivered=true, cancelled=true?
// Constrained state: exactly the valid states exist
enum ShipmentStatus { PENDING, SHIPPED, DELIVERED, CANCELLED }
final class Shipment {
private ShipmentStatus status = ShipmentStatus.PENDING;
private Instant shippedAt; // non-null iff status is SHIPPED or DELIVERED
}| State design guideline | Reason |
|---|---|
| Minimise mutable fields | Fewer states to reason about |
| Make illegal states unrepresentable (enums, sealed types) | Invariants enforced by types |
| Derive rather than store where cheap | No inconsistency between stored copies |
| Prefer immutability for values | Thread-safe, shareable, simpler |
3.2 Behavior
Behaviour is what an object does in response to messages (method calls): answering questions (queries) and changing its state or the world (commands). Behaviour defines how state may change, enforcing invariants on every transition.
public final class Order {
private final OrderId id;
private final List<OrderLine> lines = new ArrayList<>();
private OrderStatus status = OrderStatus.DRAFT;
private Instant paidAt;
public Order(OrderId id) { this.id = Objects.requireNonNull(id); }
// Command: changes state, enforces rules
public void addLine(Sku sku, int quantity, Money unitPrice) {
requireStatus(OrderStatus.DRAFT);
if (quantity <= 0) {
throw new IllegalArgumentException("quantity must be positive");
}
lines.add(new OrderLine(sku, quantity, unitPrice));
}
public void place() {
requireStatus(OrderStatus.DRAFT);
if (lines.isEmpty()) throw new IllegalStateException("empty order");
status = OrderStatus.PLACED;
}
public void markPaid(Instant when) {
requireStatus(OrderStatus.PLACED);
status = OrderStatus.PAID;
paidAt = when;
}
// Queries: no side effects
public Money total() {
return lines.stream().map(OrderLine::subtotal).reduce(Money.ZERO, Money::plus);
}
public boolean isPaid() { return status == OrderStatus.PAID; }
private void requireStatus(OrderStatus expected) {
if (status != expected) {
throw new IllegalStateException(
"expected " + expected + " but was " + status);
}
}
}| Behaviour guideline | Benefit |
|---|---|
Intention-revealing methods (place, markPaid) instead of setters | Rules in one place |
| Command–query separation | Predictable, side-effect-free queries |
| Validate on every state change | No invalid object can exist |
| Tell, don't ask (Volume I, 5.5) | Callers don't duplicate decisions |
3.3 Identity
Identity is what makes an object itself, independent of its state. Java distinguishes:

| Concept | Java | Meaning |
|---|---|---|
| Reference identity | a == b | Same object in memory |
| Equality | a.equals(b) | Considered equivalent by the class's definition |
| Hash consistency | hashCode() | Equal objects must have equal hash codes |
| Domain identity | order.id() | Business identity that persists across processes, databases and time |

| Aspect | Value object | Entity |
|---|---|---|
| Defined by | Its attribute values | A stable identity (ID) |
| Mutability | Immutable | Mutable through behaviour |
equals/hashCode | All significant fields | ID only (or reference identity) |
| Lifecycle | Created and replaced | Created, changed, archived |
| Java form | record | Class with final ID field |
| Examples | Money, Address, DateRange, Email | Customer, Order, Account |
// Value object: equality by value, provided automatically by record
public record Money(BigDecimal amount, Currency currency) {
public Money {
Objects.requireNonNull(currency);
amount = amount.setScale(currency.getDefaultFractionDigits(),
RoundingMode.HALF_EVEN);
}
}
new Money(new BigDecimal("10"), EUR)
.equals(new Money(new BigDecimal("10.00"), EUR)); // true
// Entity: equality by identity (ID), stable across state changes
public final class Customer {
private final CustomerId id;
private String name;
private Address address;
@Override public boolean equals(Object o) {
return o instanceof Customer other && id.equals(other.id);
}
@Override public int hashCode() { return id.hashCode(); }
}4How It Works Internally — Objects on the JVM
1. new allocates an object on the heap (or, after escape analysis, possibly not at all — scalar replacement).
2. Each object has a header (mark word with identity hash code, lock and GC bits; a class pointer) followed by fields. With compact object headers (JEP 519, an opt-in product feature in Java 25 via -XX:+UseCompactObjectHeaders) the header shrinks to 8 bytes on 64-bit JVMs.
3. Reference identity is the object's address/reference; System.identityHashCode is derived lazily and stored in the header.
4. equals/hashCode are ordinary methods; collections (HashMap, HashSet) rely on their contract. Mutating fields used in hashCode while the object is in a hash-based collection corrupts lookups.
5. Records generate equals, hashCode and toString from components; they are final and their fields are final.
6. Value classes (Project Valhalla, JEP 401 — in preview/early access, not final in Java 25) aim to give identity-free objects flattened layouts; treat as future direction.
4ADeep Dive — Equality Pitfalls
4A.1 The equals/hashCode contract
| Rule | Consequence of violation |
|---|---|
| Reflexive, symmetric, transitive, consistent | Collections behave unpredictably |
| Equal objects → equal hash codes | Objects "disappear" in HashSet/HashMap |
equals(null) returns false | NPEs in collections |
4A.2 Entities in JPA
@Entity
public class Customer {
@Id @GeneratedValue private Long id; // null until persisted!
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Customer c)) return false;
// transient instances are only equal to themselves
return id != null && id.equals(c.id);
}
// stable across persist
@Override public int hashCode() { return getClass().hashCode(); }
}Generated IDs are null before persisting, so the hash code must not depend on them (or use application-assigned IDs such as UUIDs assigned in the constructor — often simpler).
4A.3 Mutable value objects
// Broken: mutable key in a HashMap
Map<DateRange, Price> prices = new HashMap<>();
DateRange range = new MutableDateRange(jan1, jan31);
prices.put(range, price);
range.setEnd(feb28); // hashCode changes
prices.get(range); // null — entry is lost in the wrong bucket5APIs / Features / Syntax
| Feature | Role |
|---|---|
record | Immutable value objects with value-based equality |
| Compact constructors | Validate and normalise state at creation |
final fields and classes | Fixed identity/state, no subclass surprises |
enum | Closed set of states with behaviour |
| Sealed interfaces | Closed set of object variants |
Pattern matching instanceof / switch | Safe type-dependent behaviour; concise equals |
Objects.equals, Objects.hash | Null-safe equality helpers |
System.identityHashCode | Identity-based hash (rarely needed) |
6Practical Examples
6.1 Modelling a booking
| Concept | Kind | Why |
|---|---|---|
Booking | Entity | Has lifecycle (requested, confirmed, cancelled) and identity |
BookingReference | Value (record) | Identifier value; equality by value |
DateRange stay | Value | Two ranges with same dates are interchangeable |
Guest | Entity or value — depends on context | Entity if guests have accounts; value if just a name on the booking |
Money price | Value | Amount + currency |
6.2 Replacing setters with behaviour
// Before
booking.setStatus("CONFIRMED");
booking.setConfirmedAt(Instant.now());
booking.setRoom(room);
// After
booking.confirm(room, clock);6.3 Object design checklist
- What must this object do? (list behaviours first)
- What state does it need for those behaviours — and nothing more?
- Which states are invalid, and how does the type system exclude them?
- Is it a value (record) or an entity (ID-based equality)?
- Are all state changes performed by methods that enforce invariants?
7Common Problems & Mistakes
- Data-bag objects with getters/setters and logic elsewhere (anemic model).
- Boolean flag state allowing invalid combinations.
- Mutable value objects used as map keys.
- Entities comparing all fields in
equals(changes break identity). - hashCode based on generated IDs in JPA entities.
- Queries with side effects (
getX()that changes state).
8Performance & Production Considerations
| Aspect | Consideration |
|---|---|
| Allocation | Short-lived value objects are cheap; escape analysis may remove them |
| Header size | Compact object headers reduce memory per object for small objects |
| Immutability | Safe sharing across threads without locks |
| Identity hash | Calling identityHashCode or synchronising on objects inflates headers |
| Equality cost | Deep equals on large graphs can be expensive; keep value objects small |
9Architecture & Design Considerations
- Entities and aggregates carry identity across services only through IDs, never object references.
- Value objects in APIs: DTO records at service boundaries are values; their identity is irrelevant.
- Identity across systems: choose global IDs (UUIDs, ULIDs) when objects travel between services and databases.
- Behaviour-rich domain models keep business rules in one place, supporting SRP and testability.
10Interview Questions & Answers
What are the three characteristics of an object?
Every object has state, behaviour and identity. State is the data the object holds at a given moment, stored in its fields, such as an order's lines and status. Behaviour is what the object can do, expressed by its methods, such as adding a line or cancelling the order; good objects keep their state consistent by changing it only through behaviour. Identity is what makes an object distinct from all others, even ones with identical state: two orders with the same lines are still two orders. In Java, identity is the object reference, while domain identity is often an explicit ID.
Answer: The == operator compares references: it is true only if both variables point to the same object in memory, so it tests identity. The equals method compares logical equality, as defined by the class. For String, two different objects with the same characters are equal; for a record, two instances with equal components are equal; for a class that does not override equals, it falls back to identity, just like ==. Use equals for value comparisons and == only when you really mean the same object, or for primitives and enum constants. Whenever equals is overridden, hashCode must be overridden consistently.What is the difference between a value object and an entity?
A value object is defined entirely by its attribute values and has no identity of its own: two Money objects of 100 INR are interchangeable, so they are equal. Value objects are best made immutable, so they can be shared safely and used as map keys. An entity has an identity that persists over time and across changes to its attributes: a customer remains the same customer after changing address, and two customers with the same name are still different. Entities are usually mutable and tracked by an ID. Modelling concepts such as Money, Address and DateRange as value objects removes many bugs and simplifies entities.
Why are records good for value objects?
Records are designed for exactly what value objects need. They are immutable by default, because their fields are final, and they are final classes, so subclasses cannot add mutable state. The compiler generates equals and hashCode based on all components, which gives value-based equality, plus a readable toString. A compact constructor lets you validate and normalise inputs, so an invalid value can never be created, for example rejecting a negative amount or a currency mismatch. They also make intent obvious in code. One caution: components that are themselves mutable, such as lists, should be copied defensively, for example with List.copyOf.
What goes wrong if an object used as a HashMap key is mutated?
A HashMap stores each entry in a bucket chosen from the key's hashCode at the moment of insertion. If the key object is later mutated in a way that changes its hashCode, the map still holds the entry in the old bucket. A lookup with the same key now computes a different hash, searches the wrong bucket and returns null, even though the key is in the map, and contains or remove fail in the same way. The entry is effectively lost and the map's behaviour becomes inconsistent. The fix is to use immutable keys, such as records, Strings or IDs, never mutable entities.
How should equals/hashCode be implemented for a JPA entity with a generated ID?
Entities with database-generated IDs have no ID until they are persisted, so naive implementations break: two new entities with null IDs would be equal, or an entity's hashCode would change after saving, losing it from any HashSet it was in. A common safe approach is to compare by ID only when both IDs are non-null, treating new entities as different, and to return a constant hashCode per class, such as getClass().hashCode(), so it never changes. Alternatively, assign IDs in the application, for example UUIDs at construction, so identity is stable from the start. Avoid including mutable fields or lazy associations.
How do you prevent invalid object states?
Design objects so that invalid states cannot be represented. Keep mutable state to a minimum and prefer immutable value objects. Replace combinations of booleans and nullable fields with enums or sealed type hierarchies that model only the valid states, such as an OrderStatus or a sealed PaymentResult. Validate every input in constructors and in each method that changes state, so an object is always valid after any operation. Expose meaningful behaviour, such as order.cancel(reason), instead of setters that let callers put the object into any state. These practices move many errors from production incidents to compile time or immediate exceptions.
How does object identity matter in a distributed architecture?
Inside one JVM, identity can rely on object references. Across a distributed system it cannot: objects are copied into messages and responses, so the same customer exists as many separate copies in different services. Identity must therefore be carried by stable, globally unique IDs, such as UUIDs or well-defined business keys. Architects decide ID strategies, which concepts are entities and which are values, and which service owns each entity. Services should exchange values and IDs, not shared object graphs, and refer to other services' entities only by ID, which keeps them decoupled and avoids conflicting copies of the truth.