An Extra Join Is Not a Performance Problem
When an attribute deserves to become a subclass instead, the actual rule behind every mapping class in this series, and why redundancy needs a real, measured reason.
A movie supporting many languages raised a real question worth settling directly: does that mean Movie needs subclasses, one per language?
When Something Deserves to Be a Subclass, Not Just an Attribute
No, and the test for this is different from every enum-vs-class test so far. A subclass exists when something represents a genuinely separate category of behavior, not because a single attribute happens to hold more than one value.
A language doesn't change what a movie is or how it behaves. It's just a fact about it, so it stays a plain attribute, a list of languages on Movie, nothing more elaborate.
The Actual Rule Behind Every Mapping Class So Far
The ShowSeatType and ShowSeat classes from earlier weren't one-off decisions. They're both instances of one rule: whenever a relationship between two entities carries its own attribute, that relationship can't be a plain list or a map. It has to become its own class.
A list or a map can only point at things. It has no room to hold data about the pointing itself.
That same rule also answers a related pushback: why not make price depend on Show, Seat, and SeatType together, instead of just Show and SeatType? Go back to what the requirement actually says: a price applies to a seat type, for a specific show, full stop. It never mentioned an individual seat.
Adding an entity to a relationship that the requirement never asked for isn't extra safety, it's an assumption. The fix is checking the requirement's exact wording, not adding more entities in case they might matter.
An Extra Join Is Not a Performance Problem
Here's the one worth remembering longest. A theatre's name is only reachable from Show through Auditorium, and someone suggested storing it directly on Show too, just to skip that extra hop.
Don't, and the reasoning matters more than the answer. Redundancy is a tool for a measured performance problem you've actually found, not a habit applied because an extra join sounds slow. It usually isn't.
A single join is fast, and it's simpler than keeping two copies of the same fact in sync forever. Reaching for redundancy before anything has actually gone wrong isn't low-level design, it's optimizing a problem that doesn't exist yet.
This isn't a contradiction of the earlier point that denormalization can be a legitimate, deliberate trade-off. It's the other half of it: that trade-off is worth making once you can name the specific problem it solves, never as a reflex.
The Remaining Attributes, and the Same Rule Applied Twice More
Show also needs its own list of features, separate from the auditorium's. An auditorium might support five features, but a specific movie playing there isn't guaranteed to use all five. That's the exact same relationship as Movie having many languages while one Show uses just one: a general capability and one specific instance of it are never the same shape.
The rest of the attributes settle quickly once the same discipline gets applied to each class:
Ticketneeds an ID, an amount, the list of seats booked, a reference to the user, and the time of booking. It does not need the show's time, the movie, or the auditorium, because all three are already reachable through theShowit references — the same discipline that removed a redundant theatre name fromShowremoves these too.Userneeds an ID, a name, an email, a password, a mobile number, and age only if a requirement actually depends on it. It should never hold raw card or financial details. That data gets outsourced entirely to a payment provider, the same third party this design already agreed to rely on for payment itself.Paymentneeds an ID, an amount, and which provider handled it. A single ticket holds a list of payments rather than one, since a booking can be paid for in more than one part.
One more detail worth settling here: wherever a value looks like it might need a separate date and a separate time, like a show's start or end, a single combined date-time value is the better fit. It's what correctly handles a show that starts before midnight and ends after it, something two separate fields would quietly get wrong.
Keep reading