- Chapter
- Chapter 1 — Object-Oriented Design
- Prerequisites
- Topic 1.1
- You will be able to
- Design classes from responsibilities, model collaborations between objects, assign responsibilities with GRASP, and use inheritance only where it expresses a genuine, stable is-a relationship
- Covers
- Responsibility · Collaboration · Inheritance
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally — Classes on the JVM
- 4A. Deep Dive — Designing Classes Responsibility-First
- 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
A class is the definition of a set of objects that share the same structure (state) and behaviour. In design terms a class is best understood as a bundle of responsibilities: things it knows and things it does, fulfilled alone or through collaboration with other classes. Inheritance lets a class be defined as a specialisation of another, sharing its interface and implementation.
| Design view | Question |
|---|---|
| Responsibility | What does this class know and do? |
| Collaboration | Which other classes does it need to fulfil its responsibilities? |
| Inheritance | Is it a specialisation (is-a) of another class? |
2Why It Exists
Early OO modelling focused on nouns and data (entity-relationship thinking carried into classes). Responsibility-Driven Design (Wirfs-Brock & Wilkerson, 1989) and CRC cards (Beck & Cunningham, 1989) shifted focus to behaviour and collaboration. Craig Larman's GRASP patterns (Applying UML and Patterns, 1997) provided principles for assigning responsibilities. Meanwhile, experience with deep inheritance hierarchies in frameworks led the Gang of Four (1994) to recommend favouring composition — a theme developed in Topic 1.4.
3Core Concepts
3.1 Responsibility
A responsibility is an obligation of a class: either knowing (information it maintains or can compute) or doing (actions it performs or coordinates).
| Kind | Examples for Order |
|---|---|
| Knowing | Its lines, status, customer ID; its total (computed) |
| Doing | Adding lines, placing itself, calculating total, validating max quantity |
| Coordinating | (not Order) — CheckoutService coordinates Order, Payment and Stock |


// Information Expert: Order has the lines, so Order computes the total
public Money total() {
return lines.stream().map(OrderLine::subtotal).reduce(Money.ZERO, Money::plus);
}
// Creator: Order contains OrderLines, so Order creates them
public void addLine(Product product, int quantity) {
lines.add(new OrderLine(product.sku(), quantity, product.price()));
}
// Pure Fabrication: persistence is not a domain responsibility → repository
public interface OrderRepository {
void save(Order order);
Optional<Order> byId(OrderId id);
}| Responsibility heuristic | Rationale |
|---|---|
| Keep related knowing and doing together | High cohesion; Information Expert |
| Name classes after their responsibility | PriceCalculator, not PriceManager |
| A class's responsibilities should fit on an index card | Forces focus (SRP, Chapter 2) |
| Separate domain responsibilities from technical ones | Separation of Concerns |
3.2 Collaboration
Few responsibilities can be fulfilled by one object alone. Collaboration is objects sending messages to each other to fulfil a larger responsibility. Good collaborations are narrow (few collaborators, simple messages) and directed (dependencies point toward stable abstractions).

public final class CheckoutService { // coordinator
private final ShippingPolicy shipping;
private final PaymentGateway payments;
private final OrderRepository orders;
public CheckoutService(ShippingPolicy shipping, PaymentGateway payments,
OrderRepository orders) {
this.shipping = shipping; this.payments = payments; this.orders = orders;
}
public Receipt checkout(Order order, PaymentDetails details) {
Money amount = order.total().plus(shipping.costFor(order));
Receipt receipt = payments.charge(amount, details);
order.markPaid(receipt);
orders.save(order);
return receipt;
}
}| Collaboration quality | Signal |
|---|---|
| Few collaborators | Constructor with 2–4 dependencies, not 12 |
| Messages at the right level | order.markPaid(receipt) not order.setStatus(...) |
| Collaborators via abstractions | PaymentGateway interface, not a vendor SDK class |
| No chains through collaborators | Law of Demeter (Volume I, 5.5) |
A constructor with many dependencies is a strong signal that a class has too many responsibilities (SRP).
3.3 Inheritance
Inheritance defines a class as a subtype of another: it inherits the superclass's interface (so it can be used wherever the superclass is expected) and, for class inheritance, its implementation.

| Kind | Java | What is inherited |
|---|---|---|
| Interface inheritance | implements, interface extends | Contract only (plus default methods) |
| Implementation inheritance | extends a class | Contract, state and behaviour |
| Sealed hierarchy | sealed … permits | Closed set of known subtypes |
// Sealed hierarchy: closed, known variants — a good use of inheritance in modern Java
public sealed interface Payment permits CardPayment, BankTransfer { Money amount(); }
public record CardPayment(Money amount, CardToken token) implements Payment { }
public record BankTransfer(Money amount, Iban from) implements Payment { }| Use inheritance when… | Avoid it when… |
|---|---|
| The subclass truly is a kind of the superclass everywhere (Liskov, Chapter 4) | You only want to reuse some code |
| The superclass was designed and documented for extension | The superclass is from another team/library and not designed for it |
| The hierarchy is stable and shallow | Variation occurs in several independent dimensions |
| You want polymorphism over a closed set (sealed) | Behaviour must change at runtime |
4How It Works Internally — Classes on the JVM
1. The class loader loads a class file, verifies it and creates a Class object with metadata in Metaspace.
2. Each class has a virtual method table (vtable) for dispatch; interfaces use itable/dispatch stubs.
3. A call on a superclass or interface type uses dynamic dispatch to the runtime type's method; the JIT devirtualises monomorphic or bimorphic call sites and inlines them.
4. Constructors chain: a subclass constructor always calls a superclass constructor first — calling overridable methods from a constructor exposes partially initialised subclasses (a classic bug).
5. final classes/methods and sealed hierarchies give the JIT and compiler more certainty (exhaustive switches, easier devirtualisation).
// Constructor calling an overridable method — dangerous
class Base {
Base() { init(); } // calls subclass override before subclass fields are set
void init() { }
}
class Sub extends Base {
private final List<String> items = new ArrayList<>();
// NullPointerException: items not yet assigned
@Override void init() { items.add("x"); }
}4ADeep Dive — Designing Classes Responsibility-First
4A.1 From use case to classes
| Step | Output |
|---|---|
| 1. List system responsibilities from a use case | "validate cart", "price order", "charge payment", "reserve stock" |
| 2. Identify candidate classes from domain language | Cart, Order, PricingPolicy, PaymentGateway, Inventory |
| 3. Assign each responsibility (GRASP) | Order computes total; PricingPolicy applies discounts |
| 4. Walk through scenarios with CRC cards | Discover missing collaborators and misplaced responsibilities |
| 5. Refine: split overloaded classes, merge trivial ones | Cohesive classes with clear names |
4A.2 Class stereotypes (Wirfs-Brock)
| Stereotype | Role | Java/Spring example |
|---|---|---|
| Information holder | Knows and provides information | Records, entities |
| Structurer | Maintains relationships between objects | Aggregates, collections |
| Service provider | Performs work | PriceCalculator |
| Controller | Makes decisions, directs others | Application service |
| Coordinator | Delegates, little decision-making | Facade, orchestrator |
| Interfacer | Transforms information between system parts | Controllers, adapters, mappers |
A class with more than one stereotype (e.g. controller and interfacer and information holder) usually has too many responsibilities.
5APIs / Features / Syntax
| Feature | Design use |
|---|---|
class, final class | Service providers, entities; final unless designed for extension |
record | Information holders (values, DTOs) |
interface | Roles and collaboration contracts |
abstract class | Shared partial implementation designed for extension |
sealed … permits, non-sealed | Controlled inheritance |
@Override | Compiler-checked overriding |
| Constructor injection | Explicit collaborators |
6Practical Examples
6.1 CRC session outcome
| Class | Responsibilities | Collaborators |
|---|---|---|
| Cart | Know items; add/remove; compute subtotal | CartItem, Money |
| Order | Know lines/status; place; mark paid; total | OrderLine, Money |
| CheckoutService | Coordinate checkout | Order, ShippingPolicy, PaymentGateway, OrderRepository |
| ShippingPolicy | Decide shipping cost | Order, RateTable |
6.2 Refactoring a class with too many collaborators
// Before: 9 dependencies — a sign of several responsibilities
public OrderService(OrderRepository r, PaymentGateway p, Mailer m, PdfRenderer pdf,
TaxTable t, StockClient s, AuditLog a, Metrics mt,
FeatureFlags f) { … }
// After: responsibilities split; the service coordinates three collaborators
public CheckoutService(OrderRepository orders, PaymentGateway payments,
DomainEvents events) { … }
// Mail, PDF, audit and metrics react to OrderPaid events; tax lives in PricingPolicy6.3 Replacing implementation inheritance with interface + composition
// Before: ReportService extends BaseService to reuse logging and transactions
// After: implements its role interface and composes collaborators
public final class ReportService implements ReportGenerator {
private final ReportRepository reports;
private final ReportRenderer renderer;
public ReportService(ReportRepository reports, ReportRenderer renderer) {
this.reports = reports; this.renderer = renderer;
}
public byte[] generate(ReportId id) { return renderer.render(reports.byId(id)); }
}7Common Problems & Mistakes
- Data-first classes with getters/setters and logic elsewhere.
- Manager/Util classes accumulating unrelated responsibilities.
- Too many collaborators injected into one class.
- Inheritance for code reuse (
BaseService,AbstractControllerwith helpers). - Deep hierarchies (more than two or three levels).
- Overridable calls from constructors.
- Non-final classes by default, inviting unintended extension.
8Performance & Production Considerations
| Aspect | Consideration |
|---|---|
| Dynamic dispatch | Inlined for monomorphic/bimorphic sites; megamorphic sites (many implementations at one call site) are slower |
| Class count | More small classes → slightly more Metaspace and class-loading time; negligible for most services |
final | Helps readers and tools more than the JIT (which speculates anyway) |
| Startup | Large class graphs affect startup; AOT caches help (Book 4) |
9Architecture & Design Considerations
- Responsibility assignment scales: modules and services are also bundles of responsibilities (Chapter 2).
- Collaboration patterns within a service (coordinator + experts) mirror orchestration vs choreography between services.
- Framework base classes (inheritance-based frameworks) are a coupling risk; prefer composition-friendly frameworks.
- Code conventions:
finalby default, records for data, constructor injection for collaborators.
10Interview Questions & Answers
What is a class in object-oriented design?
A class defines a set of objects that share the same structure, their state, and the same behaviour, their methods. In design terms the more useful view is that a class is a bundle of responsibilities: things it knows and things it does, fulfilled alone or by collaborating with other classes. Rebecca Wirfs-Brock's responsibility-driven design starts from behaviour: list what the system must do, assign each responsibility to the class best placed to fulfil it, and let the fields follow. Order knows its lines and computes its total, while CheckoutService coordinates payment and stock. Starting from data instead tends to produce anemic classes with logic scattered across services.
What is a CRC card?
A CRC card is an index card with three parts: the Class name, its Responsibilities, and its Collaborators, the other classes it needs to fulfil them. Kent Beck and Ward Cunningham introduced CRC cards in 1989 as a lightweight way to design object models. A team writes one card per candidate class, then walks through a use case such as checkout, passing control from card to card. Missing collaborators, misplaced responsibilities and overloaded classes become obvious quickly. The small card is a deliberate constraint: if a class's responsibilities do not fit, it is probably doing too much. Cards are cheap to rewrite, which encourages exploring alternatives before any code exists.
What is the GRASP Information Expert principle?
Information Expert says: assign a responsibility to the class that already has the information needed to fulfil it. If Order holds its OrderLines, Order should compute its total, rather than an OrderService pulling the lines out through getters and adding them up. Keeping behaviour next to its data raises cohesion, avoids getters that expose internals, and means a change to how lines are stored stays inside one class. It is the most frequently applied of Craig Larman's GRASP principles. It is not absolute, though: if following it would drag persistence or UI concerns into a domain class, Pure Fabrication suggests a separate class, such as a repository, instead.
What is the difference between interface and implementation inheritance?
Interface inheritance shares only a contract. A class that implements an interface promises to provide certain methods and can be used polymorphically, but it inherits no state and, apart from default methods, no implementation. Implementation inheritance, extending a class, shares the contract plus fields, behaviour and the constructor chain. That makes the subclass depend on the superclass's internal details, so a change in the parent can silently break children, the fragile base class problem. Prefer interfaces plus composition for reuse and variation. Use implementation inheritance only for a genuine, stable is-a relationship where the superclass was designed and documented for extension, or for a closed sealed hierarchy.
What does a constructor with many dependencies indicate?
It usually means the class has several responsibilities. Each injected dependency is a collaborator, and when an OrderService needs nine of them, such as a repository, payment gateway, mailer, PDF renderer, tax table, stock client, audit log, metrics and feature flags, it is coordinating several unrelated concerns. Such classes change for many reasons and are hard to test. The fix is to split by responsibility: keep a coordinator with two to four collaborators, move rules such as tax into the domain object or policy that owns them, and let side effects like mail, PDF and audit react to a domain event such as OrderPaid. Hiding the dependencies behind a facade is not a fix.
Why is calling an overridable method from a constructor dangerous?
Because Java runs the superclass constructor before the subclass's field initialisers and constructor body. If Base's constructor calls an overridable method such as init(), dynamic dispatch runs the subclass's override while the subclass's fields still hold default values: null, zero or false. Even final fields are not yet assigned. In the classic example, Sub's init() adds to an items list that is still null and throws a NullPointerException, or worse, silently works on broken state. The rule is that constructors should call only private, static or final methods. If setup needs subclass input, pass it as a constructor argument or use a static factory that fully constructs the object before calling any hooks.
When are sealed hierarchies a good use of inheritance?
Sealed hierarchies are a good use of inheritance when the set of subtypes is closed and known, and the variation is part of the domain rather than an extension point for others. A Payment that is either a CardPayment or a BankTransfer, or a Redemption result with three outcomes, are typical cases. Declaring the type sealed with permits lets the compiler check exhaustive switch pattern matching, so adding a new subtype breaks every switch that does not handle it, which is exactly what you want. Records as permitted subtypes keep each variant small and immutable. If third parties must add variants, or variants change often, use an open interface with strategies instead.
How does responsibility-driven design relate to service design?
Responsibility-driven design scales from classes to services. A service, like a class, is a bundle of responsibilities, and the same questions apply: which capability does it own, what information does it need, and with whom must it collaborate? Information Expert becomes assigning a capability to the service that owns the data, so pricing lives with the catalogue data it needs rather than being assembled from remote calls. Narrow, directed collaborations become small APIs and dependencies that point towards stable services. A coordinator class mirrors an orchestrating service, while event-driven collaboration mirrors choreography. Clear ownership of each responsibility is what keeps services from becoming a chatty, tightly coupled distributed monolith.