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.

Watch it evolve1 / 9
The problem

Members book classes

A member books many sessions, and a session has many members.

80
Start
Stage 1
Final

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.

A list on each sideJava
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(", "));
    }
}
**Member− name: String− sessions: List<ClassSession>~ name(): String~ joined(ClassSession)~ left(ClassSession)~ sessions(): List<ClassSession>ClassSession− title: String− capacity: int− members: List<Member>~ title(): String~ book(Member): boolean~ cancel(Member)~ roster(): String

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 Member?

On ClassSession?

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.

Java
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;
    }
}
*«enum»BookingStatusBOOKEDWAITLISTEDATTENDEDMember− name: String~ name(): StringBooking− member: Member− status: BookingStatus~ member(): Member~ status(): BookingStatus~ promote()~ attend()ClassSession− title: String− capacity: int− bookings: List<Booking>~ title(): String~ book(Member): Booking~ cancel(Member): Booking~ attend(Member)~ roster(): String
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

book() takes a member as a parameter

*«enum»BookingStatusBOOKEDWAITLISTEDATTENDEDMember− name: String~ name(): StringBooking− member: Member− status: BookingStatus~ member(): Member~ status(): BookingStatus~ promote()~ attend()ClassSession− title: String− capacity: int− bookings: List<Booking>~ title(): String~ book(Member): Booking~ cancel(Member): Booking~ attend(Member)~ roster(): String

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

* (zero or more)

A new session has no bookings yet

Booking to its member

1

A booking always belongs to exactly one member

Booking to its session

1

A booking is in exactly one session (here, by being inside it)

Member to bookings

*, not stored

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

Booking

Status, place in the waitlist

Student and course

Enrollment

Term, grade

Reader and book copy

Loan

Due date, returned or not

Vehicle and parking spot

Ticket

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

1. Where should "Cy is waitlisted for Tuesday flow" be stored?
2. Why did the first version need both joined() and left() on Member?
3. A session creates each booking and no booking moves to another session. Which relationship fits?
4. "At most 20 people per class, but 12 in the small room." Should 20 appear as a multiplicity?

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: 1 means always exactly one, * means any number including none.
  • A limit that changes from object to object is data and code, not a multiplicity.