Responsibility-Driven Design — Junior¶
What? Responsibility-Driven Design (RDD) is Rebecca Wirfs-Brock's approach to OO design: you start by asking "who is responsible for what?" and assign each responsibility to the object best suited to own it. Objects are not records of data — they are role-players with jobs. How? For every behavior in your system, identify the responsibility it represents. Find the object that already knows the information that responsibility needs. Give the responsibility to that object. The result is a set of classes that collaborate by delegation, not by external scripts.
1. The core question: who owns this?¶
A useful design exercise: for every method you're about to write, finish the sentence "This is the responsibility of ____."
// You're about to write code that calculates an invoice total.
// Who is responsible for knowing what an invoice totals to?
//
// InvoiceService.calculateTotal(Invoice inv)? ← no, separates rule from data
// Invoice.total()? ← yes, the invoice owns its sum
// You're about to write code that decides whether a reservation has expired.
// Who is responsible for knowing if a reservation is expired?
//
// ExpiryChecker.isExpired(Reservation r)? ← no
// Reservation.isExpired(Clock now)? ← yes
The class that already holds the information needed for a decision is almost always the right owner of that decision.
2. Why responsibilities, not data?¶
Most beginners design like this:
- Identify nouns in the requirements.
- Make each noun a class.
- Give it fields matching its data attributes.
- Sprinkle methods that operate on those fields.
- Write Services to glue them together.
The result: many classes, much data, few real responsibilities. Business rules end up in services because no domain class felt like the natural home. The model is structurally OO and behaviorally procedural.
RDD inverts the order:
- Identify responsibilities — things the system has to do.
- Decide what object should own each responsibility.
- Let the data structure emerge from what each object needs to perform its job.
The first version gives you records with services. The second gives you a domain.
3. The six classic stereotypes¶
Wirfs-Brock observed that domain objects play a small number of roles — patterns of behavior she called stereotypes. Naming the stereotype helps you know what an object should and shouldn't be doing.
| Stereotype | Job | Java example |
|---|---|---|
| Information Holder | Knows facts; provides answers about itself. | Money, Address, Customer |
| Service Provider | Performs a focused service on request. | EncryptionService, PasswordHasher, TaxCalculator |
| Structurer | Maintains relationships between other objects. | Graph, Index, Catalog, Hierarchy |
| Coordinator | Reacts to events and routes work between others. | OrderSaga, PaymentOrchestrator |
| Controller | Makes decisions to govern other objects. | Order.place(), ReservationManager, traffic-light state machine |
| Interfacer | Translates between the system and the outside world. | HttpController, JpaRepository, KafkaConsumer |
A class that tries to play every role is a God Object. A class that plays no coherent role is anemic. RDD asks you to pick a stereotype per class and stay disciplined about it.
4. A worked example — a tournament chess clock¶
Requirements: a digital chess clock has two timers (one per player), a button that switches whose timer runs, and a rule that the player whose timer reaches zero loses.
Data-first approach:
public class ChessClock {
public long whiteRemainingMs;
public long blackRemainingMs;
public Color running;
public boolean gameOver;
public Color loser;
}
class ChessClockService {
void press(ChessClock c) { ... }
void tick(ChessClock c, long elapsedMs) { ... }
boolean isGameOver(ChessClock c) { ... }
}
RDD: list responsibilities, then ask who owns each.
| Responsibility | Owner |
|---|---|
| Knowing how much time each side has left | PlayerTimer (Information Holder) |
| Counting down when active | PlayerTimer (it ticks itself) |
| Switching whose timer runs | ChessClock (Controller) |
| Declaring the loser | ChessClock (Controller, when a PlayerTimer flags out) |
| Telling the UI what to render | PlayerTimer.format() + ChessClock.status() (Information Holders) |
Result:
public final class PlayerTimer {
private long remaining;
public void tick(long elapsed) {
remaining = Math.max(0, remaining - elapsed);
}
public boolean flagged() { return remaining == 0; }
public Duration remaining() { return Duration.ofMillis(remaining); }
}
public final class ChessClock {
private final PlayerTimer white;
private final PlayerTimer black;
private Color running;
private Color loser;
public void press() {
running = running == WHITE ? BLACK : WHITE;
}
public void tick(long elapsedMs) {
if (loser != null) return;
timerOf(running).tick(elapsedMs);
if (timerOf(running).flagged()) loser = running;
}
}
No ChessClockService. The timer owns ticking; the clock owns turn-switching and loss declaration. Each class knows exactly what it is responsible for.
5. The "who knows" test¶
When you're stuck on where to put a method, ask: which object already has all the information this method needs to do its work?
- Calculating shipping cost: needs item weights and dimensions → the order (which holds items) knows them. So
order.shippingCost(), notShippingCalculator.calculate(order). - Determining seat availability: needs current bookings → the flight knows them. So
flight.availableSeats(), notSeatChecker.check(flight). - Deciding whether a user can post: needs the user's role and post history → the user knows them. So
user.canPost(), notPostingPolicy.allows(user).
If two objects share the information equally, the responsibility might belong to a third (a Coordinator) — or it might be a sign that one of them should hold a reference to the other.
6. Distributing intelligence, not centralizing it¶
A common anti-pattern: one mega-class becomes "the brain" while everything else is dumb data. RDD pushes you the other way — intelligence spreads thinly across many small role-players.
// Centralized "brain":
class OrderService {
void process(Order o, Customer c, Payment p) {
if (c.isBlocked()) ...
if (o.getItems().stream().anyMatch(i -> !inventory.has(i))) ...
if (p.getAmount().compareTo(o.getTotal()) < 0) ...
if (o.getShippingAddress().getCountry().isEmbargoed()) ...
...
}
}
// Distributed responsibilities:
o.placeBy(c, p);
// ↓ delegates ↓
c.assertNotBlocked();
o.assertItemsAvailable(inventory);
p.assertCovers(o.total());
o.shippingAddress().assertNotEmbargoed();
Each check moves to whoever holds the relevant data. The order coordinates; it doesn't do everything. This is RDD's payoff: complexity is spread, not concentrated.
7. Collaboration, not invocation¶
RDD encourages thinking about objects as collaborators: they call on each other for help instead of being orchestrated by an external script.
public final class LibraryMember {
public Loan borrow(Book book, Calendar today) {
if (loansOverdue()) throw new BorrowingBlockedException();
return book.lendTo(this, today); // collaborate with Book
}
}
public final class Book {
public Loan lendTo(LibraryMember member, Calendar today) {
if (!available()) throw new BookUnavailableException();
availableCopies--;
return new Loan(this, member, today.plus(LOAN_DURATION));
}
}
LibraryMember enforces "you must not be blocked"; Book enforces "you must be available"; Loan is what they both produce. No BorrowingService orchestrates them. They talk to each other.
8. Common newcomer mistakes¶
Mistake 1: re-creating the database schema.
public class Order {
private Long id;
private Long customerId;
private String status;
private BigDecimal total;
// 25 more fields
}
You've modeled the table, not the responsibilities. Start over from "what does an order do?" and let the schema follow, not lead.
Mistake 2: a class with every stereotype.
public class Order {
public BigDecimal total() { ... } // Information Holder
public void place() { ... } // Controller
public void notifyCustomer() { ... } // Coordinator
public Order toDto() { ... } // Interfacer
public void saveToDb() { ... } // Interfacer
}
notifyCustomer and saveToDb are different roles. Split them out (event publisher; repository). Keep the order focused on being itself.
Mistake 3: misplacing the decision because the data is "easy to fetch".
class CheckoutService {
boolean canCheckout(Customer c) {
return !c.isBlocked() && c.getCart().total().compareTo(MIN_ORDER) >= 0;
}
}
The customer already knows whether it's blocked and what its cart total is. The decision belongs to the customer (customer.canCheckout()), not a service. The convenience of "I'll just grab the data and decide here" is the trap RDD asks you to resist.
Mistake 4: forcing one stereotype where two are needed.
A Reservation that both holds reservation data and coordinates a multi-step cancellation workflow with refunds, notifications, and audit trails is doing two jobs. Extract the workflow into a CancellationProcess (Coordinator) and let Reservation stay an Information Holder + Controller of its own state.
9. Quick rules¶
- For every behavior, finish the sentence "This is the responsibility of ____."
- Prefer the object that already holds the data — who knows wins.
- Name each class's primary stereotype; reject methods that don't fit it.
- Spread intelligence across collaborators; don't centralize it in a service.
- If a class plays too many roles, split it.
10. What's next¶
| Topic | File |
|---|---|
| Designing collaborations, walk-throughs, stereotype tables | middle.md |
| RDD with frameworks, ORMs, and real architecture | senior.md |
| Driving RDD across a team and codebase | professional.md |
| Hands-on RDD exercises | tasks.md |
| Interview Q&A | interview.md |
Memorize this: ask who is responsible for what. Give the work to the object that already knows the data. Name each class's role. The shape of the model comes from the responsibilities, not from the schema.