UML Class and Object Diagrams
A class diagram is a claim about source code. If the filled diamond says the garage owns its spots, the Java type must create those spots, keep them off any other aggregate, and de
UML Class and Object Diagrams
Advanced System Design & Software Architecture · Lesson 12 · Low-Level Design and Design Patterns
Lesson Overview
A class diagram is a claim about source code. If the filled diamond says the garage owns its spots, the Java type must create those spots, keep them off any other aggregate, and define what happens to a spot when the floor goes away. If the diagram and the fields disagree, the diagram is decoration, and an interviewer who asks "where is this arrow in the code?" will find the gap in under a minute.
TechLearningPro's campus garage learned the cost of a decorative diagram. The whiteboard showed ParkingFloor composed of ParkingSpot, and ParkingSpot composed of Vehicle. The second diamond was wrong: a vehicle arrives from outside, parks, and leaves. The implementation followed the diamond anyway and stored a Vehicle row with ON DELETE CASCADE from the spot. When an operator retired spot S-12 for repairs, the delete removed the member's vehicle profile, including the plate used by the gate on other days. The diagram had not distinguished ownership from a temporary link. The database had implemented the diagram faithfully.
This lesson is how to draw class and object diagrams that match a Java 21 model, and how to refuse a symbol you cannot implement. It covers attributes, operations, visibility, associations, multiplicity, navigability, the hollow diamond (aggregation) and the filled diamond (composition), generalization, realization, dependency, association classes, and qualified associations. Object diagrams are snapshots of that structure at one instant: one ticket, one spot, one plate. The running example is a single-garage parking slice, enough to force the ownership decision. The full interview case — allocation strategy, multiple vehicle sizes, payment — is lesson 20. /learn/low-level-design/class-diagrams introduces the boxes. Here the test is whether a reviewer can generate the fields from the picture and the picture from the fields.
Learning Objectives
By the end of this lesson you will be able to:
- Read a class box as name, attributes, and operations, and map
+,-, and#to Java access. - Choose association, composition, or dependency for a relationship, and refuse aggregation unless you can say what extra constraint the hollow diamond adds.
- Assign multiplicity and navigability that match a field, an
Optional, or aList, including order. - Draw realization and generalization so they correspond to
implementsandextends, including a Java 21 sealed interface. - Use an association class or a qualified association when the link itself has data or a lookup key, and keep that from being drawn twice as an attribute.
- Sketch an object diagram for one occupied spot and check it against the class multiplicities.
- Translate composition into a deletion and aliasing rule a store can enforce.
- Review a diagram for double-drawn structure, impossible multiplicity, and lifecycle arrows that point at an independent entity.
Prerequisites
- Lesson 11's lending model: you can say which type owns a rule and which type only holds an id. The same judgment decides composition versus association.
- Java 21 records and
List. You should be comfortable reading afinalfield. - UML as a picture language, not as a code generator. This lesson uses Mermaid's
classDiagram, which covers the relationships interviews require. It does not cover every UML 2.5 adornment (redefines, subsets, general ordering on association ends). Where Mermaid cannot draw a symbol, the prose states the UML name. - Lesson 8's entity versus value object. A value object is usually composed. An entity in another aggregate is usually associated by id.
Detailed Core Concepts
What a class diagram is for
A class diagram freezes the static structure: which types exist, what they remember, what they can be asked, and how they are allowed to reach each other. It does not show a request, a transaction, or a failure. Those are sequence and activity diagrams in lesson 13. Interviewers who say "draw the design" almost always want this static picture first, then one sequence for the risky use case.
The diagram is at one level of abstraction. A whiteboard for the garage shows ParkingGarage, ParkingFloor, ParkingSpot, ParkingTicket, and Vehicle (or VehicleNumber). It does not show StringBuilder. Adding every JDK type makes the ownership decision invisible. Leaving out ParkingTicket hides the fact that entry time does not belong on Vehicle.
Three compartments, top to bottom, are the class:
- Name.
ParkingSpot. An interface is marked«interface». An abstract class is marked abstract. An enumeration is marked«enumeration». - Attributes.
spotCode: String, with visibility. Derived attributes are prefixed with/in UML (/occupied: boolean) when they are computed from other state. Writing both the stored flag and the derived predicate without the slash is how two sources of truth begin. - Operations.
occupy(vehicleNumber): void. Include operations that protect invariants. Omit trivial getters on a whiteboard unless the interviewer asks. A diagram of only getters is an anemic model drawn formally.
Visibility in UML maps cleanly:
| UML | Java | When you use it |
|---|---|---|
+ public | public | The contract other packages call |
- private | private | Fields and helpers |
# protected | protected | Rare here; a subclass contract you mean |
~ package | package-private | Types inside com.techlearningpro.parking that share representation |
Mermaid's class diagram supports + and - reliably. Use them. A field drawn as +spots advertises a public mutable list, which is an encapsulation defect from lesson 11 even if the picture looks tidy.
Association is a link with an independent life
An association is a structural relationship between two types whose instances can exist apart. ParkingTicket is associated with ParkingSpot: the ticket refers to the spot, and the spot's lifecycle is the floor's, not the ticket's. When the visit ends, the ticket remains as history and the spot becomes free. Neither delete cascades into the other by default.
In Java the association is a field. Navigability is the arrow: ParkingTicket has SpotId spotId (or a ParkingSpot reference). The spot does not have to point back. A bidirectional association means two fields that must be kept consistent. That is a real cost. Prefer a single direction unless a query needs the reverse, and then decide who is the expert for the update. In the garage, the ticket points at the spot id. "Which ticket is in this spot?" is a lookup in the store, not a second field on ParkingSpot that can drift from the ticket.
Multiplicity constrains how many instances sit at an end:
1— exactly one. A ticket has one spot. The field is non-null.0..1— optional. A spot has a current ticket or none. In Java this is a nullable reference or, at the edge of the aggregate, a value that makes absence explicit.Optionalas a field is a style the JDK authors discourage; a package-private nullable reference or an empty id is enough if every reader goes throughisOccupied().1..*— at least one. A floor that can exist with zero spots is0..*, not1..*. Read the requirement before you copy a star from a template.n— a specific number, such as one entry gate in this slice.
Multiplicity is a runtime rule, not a drawing habit. 1..* on spots means ParkingFloor's constructor rejects an empty list. If the code accepts empty, change the diagram to 0..*.
Role names sit at the ends: ticket at the ticket end, spot at the spot end. They become the field names. If you cannot name the role, you do not understand the link.
Composition and aggregation
Composition (filled diamond at the owner) means a part belongs to one whole, the part's life is bounded by the whole, and the part is not a member of a second whole. Floors are composed by the garage. Spots are composed by the floor. Deleting a floor deletes its spots. A spot object is not parked in two floors. In Java:
- The owner holds the parts in a
finalcollection it created. - It does not return the live list. It returns an unmodifiable view or a stream of facts.
- A repository delete of the owner deletes the parts (
ON DELETE CASCADEonparking_spot.floor_id, or an explicit delete in the same transaction). - No other aggregate stores the same
ParkingSpotinstance as its own child.
Aggregation (hollow diamond) was intended as a weaker whole-part: the part can outlive the whole or be shared. UML never gave it precise deletion semantics. Fowler and others have said the hollow diamond has no single agreed runtime meaning. In an LLD interview, a hollow diamond that you cannot distinguish from an association is noise. Say "association" and specify multiplicity. Use the filled diamond only when you will defend cascade delete and exclusive ownership. That stance is stricter than a poster of symbols, and it is what keeps spot S-12 from deleting a vehicle.
Vehicle is not composed by ParkingSpot. The vehicle exists before entry and after exit. The spot is associated with the current VehicleNumber, a value object, or with nothing. Drawing a filled diamond there is the bug from the overview.
A value object such as VehicleNumber or Money is composed in practice: it has no identity separate from the values inside it, and it is created with its owner. You may show it as an attribute type (plate: VehicleNumber) rather than as a diamond. Both are honest. Pick one so you do not draw the value as an attribute and again as a composed class.
Dependency, generalization, realization
A dependency (dashed arrow) means one type uses another without keeping it as a field: a method parameter, a local, a thrown type. SpotAllocator.allocate(criteria) depends on Clock if the clock is an argument and not a field. Once you store the reference, upgrade the picture to an association. Leaving a stored collaborator as a dashed arrow hides a lifecycle.
Generalization (solid line, hollow triangle at the parent) is extends. Use it for a sealed hierarchy you mean, as lesson 11 did for loan status. CompactSpot, LargeSpot, and BikeSpot as subclasses of ParkingSpot are justified when behavior differs (a large spot refuses a bike-only rate) and instances do not change class. A spot that is "large" for its whole life can be a subclass or a SpotSize enum. Prefer the enum when the only difference is a value the allocator compares. Prefer subclasses when the occupy rules diverge and you want the compiler to exhaust them. Do not subclass OccupiedSpot extends ParkingSpot. Occupied is a link to a ticket, and it changes.
Realization (dashed line, hollow triangle at the interface) is implements. JdbcSpotStore realizes SpotStore. The interface lives next to the use case; the JDBC class lives outside. That is the dependency direction from lesson 4, drawn so a reviewer can see it. A diagram that shows ParkingTicket realizing SpotStore has mixed a domain fact with a fabrication.
Show a structure once
UML lets you show a relationship as an attribute or as an association line. Showing both draws two facts for one field. The rule in this course: identifiers and value types that are the whole story (spotCode: String, plate: VehicleNumber) stay in the attribute compartment. Links to other domain classes are lines, and the field is not repeated in the compartment. List<ParkingSpot> on the floor is the composition line with multiplicity 0..*, not also an attribute named spots.
An association class is a class attached to the association, used when the link has attributes that belong to the pair and not to either end alone. A stay has enteredAt and fee. Those are not properties of the vehicle (the vehicle will enter again) and not properties of the spot (the spot will host another visit). ParkingTicket is that association class, drawn between spot and vehicle number. You may also promote it to an ordinary class with two associations, which is the Java you will write. Say that promotion out loud so the interviewer knows the association class and the entity are the same design choice at two levels of formality.
A qualified association uses a key at one end. A floor qualifies its spots by spotCode. The multiplicity on the far side of the qualifier is often 0..1: given a floor and a code, there is at most one spot. In Java that is a Map<String, ParkingSpot> owned by the floor, or a unique constraint (floor_id, spot_code). The map is still composition of the spots. The qualifier is an access path, not a second owner.
Object diagrams
A class diagram is the set of possible structures. An object diagram is one structure that actually occurred. Instance specifications are written t88: ParkingTicket with the name underlined in UML, and slots show values: spotId = S-12, enteredAt = 2026-11-06T09:14:00Z. Links are instances of associations.
Object diagrams earn their place when multiplicity is easy to get wrong. Draw the Saturday morning snapshot: spot S-12 linked to ticket T-88, spot S-13 linked to nothing, and a second ticket T-40 that is closed and must not be linked as the current occupant. If your class diagram says 0..1 current ticket, the snapshot with two current tickets is illegal, and you should not "just draw both." If history tickets are a different association (* past tickets, 0..1 current), the snapshot can show both only on the history link.
Mermaid has no separate object-diagram type. This lesson uses a class diagram whose types are marked «object» and whose attributes are slot values. On a whiteboard, underline the instance name. The semantics are UML object diagrams either way.
What the diagram will not include
It will not include the allocation algorithm, tariff tables for every city, or a payment provider. Those change the sequence and the policy types. Lesson 20 adds them. This lesson's diagram is done when a reader can implement the fields, the multiplicities, and the delete behavior without inventing an arrow.
Architecture and Design Diagrams
The garage owns floors. Floors own spots. Tickets associate to a spot and carry a plate. The vehicle profile, if the campus keeps one, stays outside this aggregate and is found by plate. It is not a child of the spot.
mermaidclassDiagramclass ParkingGarage {-GarageId id+floor(code) ParkingFloor}class ParkingFloor {-FloorId id-String code+spot(code) ParkingSpot}class ParkingSpot {-SpotId id-String spotCode-SpotSize size+isOccupied() boolean+occupy(ticketId) void+vacate() void}class ParkingTicket {-TicketId id-SpotId spotId-VehicleNumber plate-Instant enteredAt-Instant exitedAt+close(at, fee) void}class VehicleNumber {<<value>>-String plate}class SpotSize {<<enumeration>>COMPACTLARGEBIKE}class SpotStore {<<interface>>+save(ParkingFloor)+findSpot(SpotId) ParkingSpot}class JdbcSpotStoreParkingGarage "1" *-- "1..*" ParkingFloor : ownsParkingFloor "1" *-- "0..*" ParkingSpot : ownsParkingTicket --> SpotIdParkingTicket --> VehicleNumberParkingSpot --> SpotSizeSpotStore <|.. JdbcSpotStore
ParkingTicket holds a SpotId, not a ParkingSpot. That is navigability toward an identity, which keeps the ticket from being a second owner of the spot object. occupy on the spot stores the current TicketId or clears it. One current ticket is 0..1, enforced in occupy (refuse if already occupied) and by a unique partial index on open tickets per spot. History is the ticket table, not a List inside the spot. Putting every historical ticket inside the spot object drags years of visits into memory to flip a boolean.
The qualifier, written in prose because Mermaid will not show a qualification box: ParkingFloor plus spotCode yields 0..1 ParkingSpot. The Java is a map built when the floor is loaded, or a query. Both must agree with a unique (floor_id, spot_code).
A snapshot at 09:14, with S-13 free and T-40 already closed, checks the multiplicities:
mermaidclassDiagramclass t88_ParkingTicket {<<object>>spotId = S-12plate = KA01AB1234exitedAt = null}class s12_ParkingSpot {<<object>>spotCode = S-12currentTicketId = T-88}class s13_ParkingSpot {<<object>>spotCode = S-13currentTicketId = null}class t40_ParkingTicket {<<object>>spotId = S-13exitedAt = 2026-11-05T18:02:00Z}t88_ParkingTicket --> s12_ParkingSpot : currentt40_ParkingTicket --> s13_ParkingSpot : history only
T-40 must not also be currentTicketId of S-13. The object diagram makes that illegal state visible. A class diagram alone, with a careless * from spot to ticket, would have allowed it.
Deletion follows the diamonds only. Removing a floor removes its spots and must fail, or must close, if any spot is occupied. Removing a spot does not remove a ticket row that is historical; the ticket keeps the spot id as a fact about the past, or the product forbids spot deletion while history exists. Say which. The vehicle profile is not in this cascade at all.
Technical Explanation
From multiplicity to a constructor
1..* floors means ParkingGarage is built with a non-empty list and offers no removeLastFloor that leaves zero. 0..* spots means a new floor may open before paint marks the bays. Encoding 1 as "we hope the list is non-empty" is how a diagram rots. The constructor throws, and a test builds a garage with zero floors and expects the exception.
0..1 current occupant is a method guard, not a comment. occupy throws if currentTicketId != null. vacate throws if it is null, unless the product treats double vacate as a harmless retry. Pick one and test it. Idempotent vacate is a reasonable desk behavior; silent double occupy is not.
Identity versus the link
SpotId on the ticket is association by identity, the same move as CopyId on Loan in lesson 11. Loading a ticket does not load the spot graph. The entry use case loads the spot, checks size, calls occupy, and saves both in one transaction. If you store a live ParkingSpot reference inside ParkingTicket and also inside ParkingFloor, you have two paths to mutate one spot. One of them will be stale. Identity association plus a single aggregate root (ParkingFloor for spot mutations, ParkingTicket for fee and exit time) keeps a writer. The ticket does not call spot.vacate(). The use case does, in an order the sequence diagram in lesson 13 will show. The class diagram's job is to make that double ownership impossible to draw without noticing.
Composition and aliases
If floor.spots() returns the internal ArrayList, a caller can add a spot the floor did not validate, or can insert the same instance into another floor. That is composition on the diagram and a public leak in the code. Return List.copyOf(spots) or an unmodifiable view, and do not document the view as a place to add. The diamond means the owner controls membership.
Sharing a ParkingSpot instance across floors fails the "one whole" rule even if both fields are final. The garbage collector does not enforce UML. Your factory does: a spot is created inside ParkingFloor.addSpot and never accepted from outside as a prebuilt instance, unless the method checks that the instance is not already parented. Creating inside the owner is the simple rule.
Enumerations and sealed sizes
SpotSize as an enum matches a closed set of painted bay types. A sealed interface is warranted when each size carries different behavior you want to dispatch, not when you only compare ranks. record Compact() implementing a sealed SpotSize with a method boolean fits(VehicleKind kind) is reasonable if bike and large rules diverge. It is unnecessary if fits is kind.ordinal() <= size.ordinal(). Interviewers sometimes want to see that you can stop at an enum. Stopping is a design decision.
JPA and SQL, checked against the diamonds
This walkthrough does not use an ORM. The mapping, if a later module adds Jakarta Persistence, must still match the picture:
- Composition of spots:
@OneToMany(mappedBy = "floor", cascade = CascadeType.ALL, orphanRemoval = true)on the floor, and a@ManyToOnethat is not optional on the spot.orphanRemovaldeletes a spot removed from the collection. Without it, the diamond is a lie. - Association by id from ticket to spot: a column
spot_idwith a foreign key.on deleteisrestrictif history must survive, notcascadefrom spot to ticket, and nevercascadefrom spot to avehicletable. - The qualifier:
unique (floor_id, spot_code).
If you use JdbcClient, the same rules are explicit SQL in the store fabrication. The class diagram does not mention JdbcClient. The store interface on the diagram is the boundary. Lesson 11 called that type a pure fabrication; the stereotype «interface» plus a realizing class is how it looks in UML.
Derived occupied
/occupied can be drawn as a derived attribute: true when currentTicketId is non-null. Storing occupied and currentTicketId invites a spot that is free and still points at T-88. One field is the source. The other is a method. The slash on the diagram is the reminder.
Practical Implementation or Design Walkthrough
Problem
Draw and implement the static model for one TechLearningPro campus garage: floors own spots, a ticket records a stay against a spot id and a plate, a spot has at most one current ticket, and deleting a spot cannot delete a member's vehicle record because the vehicle is not part of this model.
Requirements that affect the picture
- A garage has at least one floor. A floor may have zero spots on the day it is created.
- Spot codes are unique per floor.
- A spot has a size. This slice does not allocate; it only refuses
occupywhen already occupied. - A ticket stores entry time, optional exit time, and a fee set at close. Fee math can be a fixed amount passed in. Tariff policy is lesson 20.
- Plates are values. No
Vehicleentity in this module.
The types the diagram promised
javapackage com.techlearningpro.parking;import java.time.Instant;import java.util.ArrayList;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import java.util.Objects;public record VehicleNumber(String plate) {public VehicleNumber {if (plate == null || plate.isBlank()) {throw new IllegalArgumentException("plate");}plate = plate.trim().toUpperCase();}}public enum SpotSize { BIKE, COMPACT, LARGE }public final class ParkingSpot {private final SpotId id;private final String spotCode;private final SpotSize size;private TicketId currentTicketId;public ParkingSpot(SpotId id, String spotCode, SpotSize size) {this.id = Objects.requireNonNull(id);if (spotCode == null || spotCode.isBlank()) {throw new IllegalArgumentException("spotCode");}this.spotCode = spotCode;this.size = Objects.requireNonNull(size);}public boolean isOccupied() {return currentTicketId != null;}public void occupy(TicketId ticketId) {Objects.requireNonNull(ticketId);if (currentTicketId != null) {throw new IllegalStateException("occupied");}currentTicketId = ticketId;}public void vacate(TicketId ticketId) {if (!ticketId.equals(currentTicketId)) {throw new IllegalStateException("ticket mismatch");}currentTicketId = null;}public SpotId id() { return id; }public String spotCode() { return spotCode; }public SpotSize size() { return size; }public TicketId currentTicketId() { return currentTicketId; }}
vacate requires the ticket id so a stale request cannot clear a newer occupant. That guard is the 0..1 current link, implemented.
javapublic final class ParkingFloor {private final FloorId id;private final String code;private final Map<String, ParkingSpot> spotsByCode = new LinkedHashMap<>();public ParkingFloor(FloorId id, String code) {this.id = Objects.requireNonNull(id);if (code == null || code.isBlank()) {throw new IllegalArgumentException("code");}this.code = code;}public ParkingSpot addSpot(SpotId id, String spotCode, SpotSize size) {if (spotsByCode.containsKey(spotCode)) {throw new IllegalArgumentException("duplicate spot code");}ParkingSpot spot = new ParkingSpot(id, spotCode, size);spotsByCode.put(spotCode, spot);return spot;}public ParkingSpot spot(String spotCode) {ParkingSpot spot = spotsByCode.get(spotCode);if (spot == null) {throw new IllegalArgumentException("unknown spot");}return spot;}public List<ParkingSpot> spots() {return List.copyOf(spotsByCode.values());}public FloorId id() { return id; }}
addSpot constructs the child. Callers cannot insert a spot that already lives on another floor unless they already hold the instance, and nothing in this API accepts an external ParkingSpot. spots() copies the list. The map is the qualified association: code to at most one spot.
javapublic final class ParkingTicket {private final TicketId id;private final SpotId spotId;private final VehicleNumber plate;private final Instant enteredAt;private Instant exitedAt;private Money fee;private ParkingTicket(TicketId id, SpotId spotId, VehicleNumber plate, Instant enteredAt) {this.id = id;this.spotId = spotId;this.plate = plate;this.enteredAt = enteredAt;}public static ParkingTicket open(TicketId id, SpotId spotId, VehicleNumber plate, Instant at) {Objects.requireNonNull(at);return new ParkingTicket(id, spotId, plate, at);}public void close(Instant at, Money fee) {if (exitedAt != null) {throw new IllegalStateException("closed");}if (at.isBefore(enteredAt)) {throw new IllegalArgumentException("exit before entry");}this.exitedAt = at;this.fee = Objects.requireNonNull(fee);}public boolean isOpen() { return exitedAt == null; }public SpotId spotId() { return spotId; }public VehicleNumber plate() { return plate; }}
There is no Vehicle field and no setter for spotId. The association is fixed at open. A transfer between spots would be a new operation with its own invariant, not a field assignment. Leave it out until lesson 20 needs it.
A test that the snapshot is legal
javaimport static org.junit.jupiter.api.Assertions.assertFalse;import static org.junit.jupiter.api.Assertions.assertThrows;import static org.junit.jupiter.api.Assertions.assertTrue;import org.junit.jupiter.api.Test;class SpotOwnershipTest {@Testvoid spotHasOneCurrentTicketAndFloorOwnsCreation() {ParkingFloor floor = new ParkingFloor(new FloorId("F1"), "L1");ParkingSpot spot = floor.addSpot(new SpotId("S-12"), "S-12", SpotSize.COMPACT);assertFalse(spot.isOccupied());spot.occupy(new TicketId("T-88"));assertTrue(spot.isOccupied());assertThrows(IllegalStateException.class, () -> spot.occupy(new TicketId("T-89")));assertThrows(IllegalArgumentException.class,() -> floor.addSpot(new SpotId("S-99"), "S-12", SpotSize.LARGE));}}
The duplicate code test is the qualifier. The second occupy is the 0..1 end. Neither test needs Spring.
What a reviewer compares
Read the class diagram, then the fields. ParkingFloor has no List<ParkingTicket>. ParkingTicket has no ParkingSpot. ParkingSpot has no Vehicle. ParkingGarage is omitted in the snippet; its constructor takes List<ParkingFloor> and rejects empty, matching 1..*. If the diagram grows a diamond from spot to vehicle, this test suite will not catch it. A review checklist item will: "every filled diamond has a cascade or a factory, and no diamond points at an independent entity."
Enterprise Real-World Scenarios
The diamond that deleted a plate
The campus garage diagram composed Vehicle inside ParkingSpot. The first store implementation used a foreign key from vehicle to spot with ON DELETE CASCADE because "the diagram says composition." Retiring S-12 for line painting deleted the vehicle row for KA01AB1234. The member's other bookings, which looked the plate up by that row, failed at the gate the next morning. Support restored a backup of one row and lost the afternoon.
The corrected diagram associates a stay with a VehicleNumber value and keeps member vehicle profiles in the membership context, addressed by plate. Spot deletion is RESTRICT when an open ticket references the spot, and historical tickets keep spot_id even if the spot is later archived (spot.archived_at), so the foreign key still holds. The object diagram for "S-12 retired, T-88 already closed" shows a ticket whose spot id still resolves to an archived spot, not a cascaded delete of a person. Membership and parking are different aggregates. Lesson 8 named that boundary; the diamond violated it.
Two pictures of the same list
A later redesign drew spots: List<ParkingSpot> in the attribute compartment and a composition line to ParkingSpot. Two engineers implemented both. One wrote an @OneToMany collection. The other wrote a spot table and also a JSON column on floor that mirrored codes "for the diagram's attribute." They drifted within a week: the JSON listed S-14, the table had vacated it. The gate trusted the JSON.
The repair was the rule in this lesson: the link is a line, the qualifier is a unique key, and there is no second stored collection. The JSON column was dropped. The review now asks "is this attribute also a line?" before merge. The incident is why this course treats a class diagram as a claim about one representation, not as a poster that can repeat itself.
Best Practices
- Draw types that own rules. Leave JDK classes off the whiteboard.
- Show each relationship once: attribute compartment or association line.
- Use a filled diamond only with exclusive ownership and a delete rule you can state.
- Treat a hollow diamond as a prompt to redraw the link as an association with multiplicity, unless you can say what the hollow diamond forbids that the association does not.
- Prefer one navigable direction. A back-pointer needs an owner for consistency.
- Associate other aggregates by id. Compose value objects and true parts.
- Put link data (entry time, fee) on a ticket type, whether you call it an association class or an entity.
- Make multiplicity true in constructors and guards, with a test for the boundary (
0,1, and the second occupant). - Mark derived state with a method, and do not store it beside its inputs.
- Return copies or unmodifiable views from composed collections.
- Keep
«interface»on ports and realization on the JDBC type, outside the domain package. - When the picture and the fields disagree, change one of them in the same pull request.
Common Mistakes and Anti-Patterns
Composition of an independent entity. A filled diamond from spot to vehicle, implemented as cascade delete. The part outlives the whole in the real world, so the symbol was the wrong one. Draw an association to a value or to an id in another context.
Hollow diamonds on every collection. Aggregation as decoration, with no lifecycle story. Reviewers cannot tell a shared list from an owned one. Use association or composition and say which.
Attribute and line for the same field. A list drawn twice, then stored twice. Pick the line for domain types.
Multiplicity as ornament. 1..* in the picture, an empty list allowed in the constructor, a second current ticket allowed by occupy. The diagram stops being a review tool. Encode the ends you draw.
Bidirectional fields with no expert. Ticket points at spot and spot points at a mutable ticket object, updated by whoever remembers. One side will be stale after a partial save. Navigate one way, or update both in one use-case method and one transaction.
Subclass per state. OccupiedSpot extends ParkingSpot. The instance would have to change class at the gate. Use an association to the current ticket id.
God diagram. Payment, tariffs, membership, paint maintenance, and HTTP DTOs on one page. The ownership question disappears. Split by aggregate. One diagram per aggregate is a feature.
Public fields because the tool exported them as +. Generated diagrams that mark every JPA field public. Fix the generator or redraw. Visibility is part of the claim.
Troubleshooting and Design Review
Symptom: Deleting a spot removes member data.
Likely cause: a composition diamond or a cascade crosses a context boundary.
Investigation: foreign keys with ON DELETE CASCADE starting at parking_spot. Compare to the class diagram's diamonds.
Corrective action: association by value or id; RESTRICT or archive; remove the diamond.
Symptom: A spot is free in the app and occupied in the table, or the reverse.
Likely cause: occupied stored beside currentTicketId, or two representations (JSON plus rows).
Investigation: writers of each field. Search for a second collection of spots.
Corrective action: one source of truth, derived isOccupied(), drop the mirror column.
Symptom: Two open tickets for one spot.
Likely cause: multiplicity * in the model, or occupy without a guard and without a unique partial index.
Investigation: the object diagram you wish you had drawn for the incident, then the constraint list.
Corrective action: 0..1 in the method and in the database. Add the test from this lesson.
Symptom: The whiteboard cannot be implemented because every class points at every other class.
Likely cause: navigability in both directions by default, plus entities passed where ids would do.
Investigation: for each arrow, name the field. If you cannot, delete the arrow.
Corrective action: redraw with ids across aggregates and diamonds only downward inside one aggregate.
Symptom: Reviewers argue about a hollow diamond for a week.
Likely cause: the symbol has no agreed consequence.
Investigation: ask what delete and sharing behavior is required.
Corrective action: replace it with association or composition and a sentence in the decision record.
Review questions: Which fields implement this line? What is deleted with the whole? Can the part be reached from a second owner? Does this multiplicity throw in a constructor or a guard? Is the same structure drawn twice? Does the object snapshot you fear violate an end of the association?
Interview Questions and Answers
Q1. What is the difference between association and composition in a parking model?
Association means both objects have independent lives and the link can end without destroying either. A ticket refers to a spot id; closing the ticket vacates the spot and keeps both records. Composition means the part belongs only to that whole and does not outlive it. Floors own spots. Deleting a floor deletes spots, and a spot is created by the floor, not passed in from another floor. I implement composition as a final structure the owner builds, returned only as a copy, with a database cascade or a transactional delete that matches the diamond.
Q2. Why do many designers avoid the hollow diamond?
UML aggregation does not define deletion or sharing precisely enough to review. I have seen it used for every List. I draw an association with multiplicity, or a filled diamond when I will defend exclusive ownership. If a reviewer wants a hollow diamond, I ask what happens on delete and whether the part may belong to two wholes. The answers pick association or composition. The hollow diamond does not add a third rule I can test.
Q3. How do you map multiplicity to Java?
1 is a non-null field checked in the constructor. 0..1 is absence represented in one way and read through a method such as isOccupied(). 0..* is a List or a map that may be empty. 1..* rejects empty at construction. I also say whether the collection is ordered. A LinkedHashMap preserves qualifier order for spot codes; a HashMap would be an undocumented change. Multiplicity I draw and do not enforce is a defect.
Q4. When do you show a field as an attribute instead of a line?
When the type is a value or a primitive and there is no independent identity to navigate: spotCode: String, plate: VehicleNumber, size: SpotSize. When the type is another domain class with its own lifecycle, I draw a line and I do not repeat the field in the attribute box. Drawing both produced a JSON mirror of our spot table that drifted from the rows.
Q5. What does an object diagram catch that a class diagram misses?
The class diagram says what is allowed. The object diagram is one Saturday morning: T-88 current on S-12, S-13 free, T-40 closed and only historical. If I can draw two current tickets on S-13, the multiplicity is wrong or I am violating it. I use the snapshot to test the picture against a story, then I keep the illegal snapshot out of the implementation with a guard and a unique index.
Q6. What is an association class, and how does it look in Java?
It is a type for data that belongs to the link. Entry time and fee belong to a stay, not to the vehicle and not to the painted spot. In UML I can attach ParkingTicket to the association. In Java I make ParkingTicket an ordinary class with a SpotId and a VehicleNumber, created by open and closed once. I mention both so the interviewer sees I am not inventing a framework type. There is no association-class keyword in Java.
Q7. How do you model "find a spot by code on a floor"?
As a qualified association: floor plus spotCode yields 0..1 spot. The qualifier is a Map inside the floor or a unique (floor_id, spot_code). It does not create a second owner. addSpot rejects a duplicate code. I still compose the spot in the floor; the map is the access path.
Q8. How should a class diagram relate to Spring and JPA?
The domain picture shows types and ownership. Spring beans are not on it unless the discussion is the composition root. If we use Jakarta Persistence later, @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) must match a filled diamond, and a ticket's spot_id foreign key must not cascade into a membership table. I would rather show a SpotStore interface realized by a JDBC class than annotate entities on the whiteboard and call that the design.
Q9. A candidate draws OccupiedSpot extends ParkingSpot. What do you say?
Occupied is not a subtype. It is a spot plus a current ticket id, and it changes when the car leaves. Subclassing would require changing the instance's class at the gate. I keep one ParkingSpot with occupy and vacate. I subclass, or I use an enum, only for differences that last the spot's life, such as painted size, and only if behavior actually diverges.
Q10. What do you leave off the diagram in a 45-minute interview?
Getters, setters, String, equals, and the tariff engine, until the structure is agreed. I also leave off bidirectional arrows I cannot update in one place. I state the deletion rule for each diamond in a sentence. Lesson 20's allocation policies can be a single SpotAllocator dependency on a later sequence. Stuffing them into the class picture before ownership is clear is how vehicle-composed-in-spot survives review.
Q11. How do you review a diagram checked into the repository?
I diff it against the fields. Every line names a field or a map. Every diamond names a factory and a delete rule. Every multiplicity has a test at the boundary. Object snapshots for the incident we fear are either legal or rejected by a guard. If the picture is generated and marks every field public, I do not treat it as the design. I redraw the aggregate.
Hands-On Exercises
Exercise: Class and object diagrams for one garage
Problem statement. Draw the static model for a campus garage and implement the ownership rules. Floors own spots. A ticket records a stay by spot id and plate. A spot has at most one open visit. Deleting a spot must not imply deleting a member vehicle, because that entity is out of scope.
Tasks.
- List multiplicities you will enforce, including floor-to-spot and spot-to-current-ticket.
- Draw a class diagram. Use composition, association by id, an enum or sealed size, and a store interface. Do not also list
spotsas an attribute. - Draw an object diagram with one occupied spot, one free spot, and one closed ticket. Explain why the closed ticket is not current.
- Implement
ParkingFloor.addSpot,ParkingSpot.occupy/vacate, andParkingTicket.open/closeso the diagram's ends throw when violated. - State the SQL or foreign-key rule for spot deletion versus ticket history, in words.
- Show a wrong diamond (spot composed of vehicle) and the failure it causes.
- Note one relationship you represented as a dependency rather than a field, and why.
Expected deliverables. Two diagrams, the Java types, the boundary tests, and the deletion note.
Evaluation rubric.
| Assessment area | Weight | What "good" looks like |
|---|---|---|
| Ownership | 25% | Filled diamonds match factories and deletes; vehicles are not children of spots |
| Multiplicity | 20% | Empty garage floors, duplicate codes, and a second occupant fail in code |
| Diagram fidelity | 20% | Each line is one field; no doubled collections |
| Snapshot | 15% | The object diagram is a legal instance of the class diagram |
| Scope | 10% | Tariffs and payment are named as out of scope |
| Store boundary | 10% | Persistence realizes an interface and does not redefine ownership |
A diagram that compiles in a drawing tool and cannot be walked field by field misses the exercise.
Lesson Summary
Class diagrams describe types, fields, and allowed links. Object diagrams describe one legal snapshot. Association means an independent lifecycle, usually an id across aggregates. Composition means exclusive ownership, a factory inside the owner, and a delete rule. Aggregation is the symbol this lesson will not use unless its consequences are written down. Multiplicity is a constructor or a guard, not a caption. Show each structure once. In the garage, floors own spots, tickets point at spot ids and plates, and a member's vehicle profile lives somewhere else so retiring a bay cannot erase it. Lesson 13 draws the entry and exit sequences that this structure makes possible, including the failure where the spot is taken between the read and the occupy.
Further Reading
- Object Management Group, UML 2.5.1 specification — class, association, composition, and instance specification. The hollow diamond's weak semantics are visible in how little the spec constrains aggregation.
- Martin Fowler, UML Distilled — the short reading on which arrows matter, and on showing a relationship once.
- Craig Larman, Applying UML and Patterns — class diagrams tied to responsibility, which is lesson 11's GRASP applied to a picture.
- Eric Evans, Domain-Driven Design — aggregates as the ownership boundary the filled diamond must respect. See /learn/advanced-system-design/domain-driven-design-fundamentals.
- Jakarta Persistence, entity relationships —
cascadeandorphanRemovalas the persistence reading of composition, when you use them. - Primers: /learn/low-level-design/class-diagrams, /learn/low-level-design/uml-intro.
- This course: /learn/advanced-system-design/object-oriented-design-fundamentals, /learn/advanced-system-design/uml-sequence-and-activity-diagrams, /learn/advanced-system-design/lld-case-study-parking-lot.