Overview
Step 3: assign behavior to entities. Use the Information Expert principle (give a responsibility to the class that has the data needed), keep each class cohesive (SRP), and make services thin orchestrators for use cases that span several entities. The goal is a rich domain model where rules live next to the data they protect.
For each use case step, ask 'who knows?' and 'who should enforce this rule?'. Show knows its seats, so Show decides whether seats can be held. Hold knows its expiry, so it answers isExpired(). BookingService coordinates holding, paying, and confirming but does not contain seat rules.
You ask the person who has the information to make the decision: the accountant approves budgets, the designer approves layouts. The project manager coordinates but does not do everyone's job.
When to use it
- After listing entities, before coding.
- When a service class is growing too large.
- When the same rule appears in multiple places.
Where it shows up in interviews
Recognize it when: rules scattered in services.
- Design a movie ticket system
- Design a library system
- Design Splitwise
Where it is used in real software
Craig Larman's Information Expert, Creator, Controller, Low Coupling, and High Cohesion guide responsibility assignment.
Teams role-play use cases, passing responsibilities between cards.
Martin Fowler warns against entities that are only data with logic in services.
Key terms
- Information Expert
- Assign to the class with the needed information.
- Creator
- The class that contains or aggregates B should create B.
- Controller
- A service handling a use case's system events.
- Cohesion
- How focused a class's responsibilities are.
How it works, step by step
- 1List use case steps
hold seats, compute price, charge, confirm.
- 2For each step ask 'who knows?'
Show knows seats; PricingStrategy knows prices.
- 3Assign rule enforcement to the owner
Show.hold() rejects unavailable seats.
- 4Keep orchestration in services
BookingService calls Show, Payment, Hold.
- 5Check cohesion
Each class's methods relate to one concept.
Responsibility assignment
Movie ticket use cases
| Responsibility | Assigned to | Why |
|---|---|---|
| Are these seats available? | Show | Owns ShowSeats |
| Mark seats held/booked | Show | Protects seat invariants |
| Is this hold expired? | Hold | Knows its expiry |
| Price for a seat | PricingStrategy | Varies independently |
| Charge the customer | PaymentGateway | External system |
| Run the booking flow | BookingService | Coordinates several objects |
NOWResponsibility: Are these seats available? | Assigned to: Show | Why: Owns ShowSeats
BookingService stays short because each rule lives with its expert.
Implementation
class Hold { constructor(readonly id: string, readonly showId: string, readonly userId: string, readonly seatIds: string[], readonly expiresAt: number) {} isExpired(now: number) { return now >= this.expiresAt; } // expert: knows its expiry} class Show { private status = new Map<string, "available" | "held" | "booked">(); constructor(readonly id: string, seatIds: string[]) { seatIds.forEach((s) => this.status.set(s, "available")); } hold(seatIds: string[]) { // expert: owns seat state if (!seatIds.every((s) => this.status.get(s) === "available")) throw new Error("Seat unavailable"); seatIds.forEach((s) => this.status.set(s, "held")); } book(seatIds: string[]) { if (!seatIds.every((s) => this.status.get(s) === "held")) throw new Error("Seats not held"); seatIds.forEach((s) => this.status.set(s, "booked")); } release(seatIds: string[]) { seatIds.forEach((s) => this.status.get(s) === "held" && this.status.set(s, "available")); }} class BookingService { // controller: orchestrates only constructor(private shows: Map<string, Show>, private pay: (u: string, c: number) => Promise<boolean>, private now = () => Date.now()) {} async confirm(hold: Hold, amountCents: number) { const show = this.shows.get(hold.showId)!; if (hold.isExpired(this.now())) { show.release(hold.seatIds); throw new Error("Hold expired"); } if (!(await this.pay(hold.userId, amountCents))) { show.release(hold.seatIds); throw new Error("Payment failed"); } show.book(hold.seatIds); }}Complexity and performance
Orchestration only.
High cohesion.
Trade-offs
Rich models protect invariants and scale with complexity; transaction scripts are simpler for trivial CRUD.
Placing logic with the expert may add dependencies; watch for cycles.
Variants and related techniques
For rules that belong to no single entity (transfer between accounts).
Strategy objects for rules that vary (pricing, refunds).
Common mistakes
- Anemic entities with all rules in services.
Fix: Move rules to the entity that owns the data.
- God service.
Fix: Split by use case and delegate to entities and policies.
Interview questions
How do you decide which class gets a method?
Apply Information Expert: the class that has the data needed to fulfill the responsibility should own it, as long as that keeps cohesion high and coupling low. Cross-entity workflows go to a thin service.
What is an anemic domain model?
Entities that are just data with getters and setters, while all business logic lives in services. It scatters rules and makes invariants easy to break.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Assign responsibilities for Splitwise | Medium | Expert vs service. |
| Refactor an anemic Order model | Medium | Move rules into entities. |