Low-level designCore8 min
Modeling entities and relationships
Pick the things, then decide how each pair relates: who owns whom, how many, and which way the link points. When a relationship has data of its own, it's a thing too, and it gets a class.
Members book classes
A member books many sessions, and a session has many members.
1 · The situation
Members book classes
A yoga studio runs classes such as Tuesday flow, each with a fixed number of mats. Members book a place, and can cancel. Two things, a member and a class session, in a many-to-many relationship: a member books many sessions, and a session has many members.
The first version models it the most direct way: each ClassSession keeps a list of its members, and each Member keeps a list of their sessions, so either side can answer "who's in this class?" or "what classes do I have?". Python writes the methods in snake case, as usual.
class Member {
private final String name;
private final List<ClassSession> sessions = new ArrayList<>();
Member(String name) {
this.name = name;
}
String name() {
return name;
}
// Kept in step by ClassSession.
void joined(ClassSession s) {
sessions.add(s);
}
void left(ClassSession s) {
sessions.remove(s);
}
List<ClassSession> sessions() {
return List.copyOf(sessions);
}
}
class ClassSession {
private final String title;
private final int capacity;
private final List<Member> members = new ArrayList<>();
ClassSession(String title, int capacity) {
this.title = title;
this.capacity = capacity;
}
String title() {
return title;
}
boolean book(Member m) {
if (members.size() >= capacity) return false;
members.add(m);
m.joined(this);
return true;
}
void cancel(Member m) {
members.remove(m);
m.left(this);
}
String roster() {
return members.stream().map(Member::name).collect(Collectors.joining(", "));
}
}Drawn from the code: two lines, one each way. Each side holds many of the other.
Ana books Tuesday flow: booked Ben books Tuesday flow: booked Cy books Tuesday flow: full Ben cancels Tuesday flow Tuesday flow: Ana Ben's classes: 0
Cy is turned away when the class is full; when Ben cancels, both sides must forget each other.
2 · The change
A waitlist, and who actually came
The studio wants two things: when a class is full, members join a waitlist and move up when someone cancels; and after class, the teacher marks who attended. Try to place each new fact in the current model:
Fact | On | On |
|---|---|---|
Cy is waitlisted for Tuesday flow | Not about Cy alone: Cy may be booked elsewhere | Not about the session alone: it has many members |
Ana attended Tuesday flow | Ana attends some classes and misses others | Some members came, some didn't |
Cy was the first to join the waitlist | Only meaningful within one session's waitlist | Needs an order across members |
Each fact is about a pair: one member and one session. The model has no place for a pair, so the only option is more parallel lists, a waitlist and an attended list on the session and matching ones on the member, each kept in step by hand. Every booking change would then touch four lists in two classes.
The two-way link is a cost even today. book() and cancel() must update both sides, and any code that forgets the second update leaves the model contradicting itself. The practice checks flag it on this version: Member and ClassSession point at each other.
3 · The fix
Give the relationship its own class
When a relationship has data of its own, it's a thing in the model. A member's place in a session is a booking, and the waitlist and attendance are its status. Make the change in three steps:
Booking holds one member and a BookingStatus: booked, waitlisted or attended. The facts that had no home are now fields of a booking.
enum BookingStatus { BOOKED, WAITLISTED, ATTENDED }
class Booking {
private final Member member;
private BookingStatus status;
Booking(Member member, BookingStatus status) {
this.member = member;
this.status = status;
}
Member member() {
return member;
}
BookingStatus status() {
return status;
}
void promote() {
status = BookingStatus.BOOKED;
}
void attend() {
status = BookingStatus.ATTENDED;
}
}Ana books Tuesday flow: booked Ben books Tuesday flow: booked Cy books Tuesday flow: waitlisted Ben cancels Tuesday flow: Cy is booked Tuesday flow: Ana attended, Cy booked
Cy joins the waitlist and moves up when Ben cancels; Ana is marked as attended.
4 · Choosing a relationship
Four questions for every pair
For each pair of classes that must know about each other, answer these in order. The first yes decides the line; the page UML class diagrams shows how each one is drawn.
Question | If yes | In the studio |
|---|---|---|
Is A a kind of B? | Generalization | Nothing here; a private lesson might be a kind of session |
Does A create B, keep it, and end its life? | Composition | A session creates and keeps its bookings |
Does A keep a reference to a B that lives on its own? | Association, or aggregation for a group | A booking refers to its member |
Does A only use B during a call? | Dependency |
|
The new model: ClassSession is composed of bookings, each Booking refers to one Member, and nothing points back.
5 · Multiplicity
How many, at each end
A multiplicity says how many objects can sit at one end of a relationship, and each one is a rule the code must keep:
Link | Multiplicity | The rule it states |
|---|---|---|
Session to its bookings |
| A new session has no bookings yet |
Booking to its member |
| A booking always belongs to exactly one member |
Booking to its session |
| A booking is in exactly one session (here, by being inside it) |
Member to bookings |
| A member can have many; found by asking the sessions |
Not every number is a multiplicity. "At most 20 booked per class" is a business rule that changes per session, so it lives in code (capacity), not on the diagram, where the line stays *.
6 · Direction
One way by default
A link that can be followed both ways is two references that must always agree. The first version paid for it in every method that changed a booking. Point each link one way, in the direction the code actually asks its questions, and add the way back only when a use case needs it.
When it does, for example a page listing a member's bookings across the week, there are two cheaper options than a second list: ask the sessions ("which of you has a booking for Ana?"), or keep an index in one place that's updated with every booking. Either way, one place stays the source of truth.
7 · Spotting pairs
Nouns that name a relationship
Many of the most useful classes in a model aren't physical things but relationships with data. Their names give them away: a noun for the link between two other nouns.
Between | The relationship's class | Its data |
|---|---|---|
Member and session |
| Status, place in the waitlist |
Student and course |
| Term, grade |
Reader and book copy |
| Due date, returned or not |
Vehicle and parking spot |
| Entry time, fee |
UML has a notation for this, an association class: a class box joined by a dashed line to the line between the two classes. In code it is simply a class that holds both ends, or one end when its owner holds the other, as ClassSession does here.
8 · In practice
A ticket is a pair
In the parking-lot problem, a ticket links a vehicle to a spot for a stay, with an entry time and a fee. The checks expect each level to hold its spots, each ticket to know its spot so checkout can free it, and they flag two-way links that nothing needs.
Design the parking lot and model its pairs.
Check yourself
4 questions
Takeaways
Remember this
- For each pair of classes, decide: kind of, owns, refers to, or only uses.
- A fact about a pair of things belongs to neither; give the pair a class, such as a booking.
- Point each link one way, in the direction the code asks its questions; a two-way link is two references to keep in step.
- Multiplicities are rules:
1means always exactly one,*means any number including none. - A limit that changes from object to object is data and code, not a multiplicity.