UML class diagrams: a practical introduction
How to read and draw UML class diagrams: compartments, visibility markers, the five relationships and their exact notation, multiplicity and how much detail to show.
The DiagramDock team · · 10 min read
Most class diagrams fail in one of two ways. Either they are so detailed that nobody reads them, or they use the notation loosely enough that two people looking at the same diagram come away with different designs. A hollow diamond and a filled one look nearly identical at a glance, and they mean different things about who owns what.
This article covers the part of UML class notation you will use day to day: what goes in each box, how to mark visibility and types, the five relationships and exactly where their markers sit, multiplicity, and how to decide what to leave out. It finishes with a worked example built from scratch.
What a class diagram is for
A class diagram shows the static structure of a system: the kinds of things it knows about, what each one holds, what each one can do, and how they relate. It says nothing about the order in which things happen. That is the job of a sequence diagram or an activity diagram.
UML itself grew out of the work of Grady Booch, James Rumbaugh and Ivar Jacobson in the mid-1990s and was adopted as a standard by the Object Management Group in 1997. The full specification is large. The class diagram subset that working teams actually use is small enough to learn in an afternoon, and it is worth learning precisely, because the value of a shared notation is that it means the same thing to everyone.
The most useful moment to draw one is before the code exists, when moving a box costs nothing. The second most useful moment is when someone new joins and needs to understand a domain model that has grown over several years.
Compartments, visibility and types
A class is drawn as a rectangle split into horizontal compartments. The top compartment holds the class name, in the singular and in title case: Order, not Orders. The middle compartment lists attributes. The bottom compartment lists operations, which most people call methods. Either of the lower compartments can be left empty or omitted when it adds nothing.
Each attribute is written as a name followed by a colon and a type, such as email: string or dueAt: Date. Operations follow the same pattern with parameters in brackets and an optional return type after the closing bracket: isOverdue(today: Date): boolean. Types are worth including whenever there is any doubt. "price" invites the question of whether it is a float, an integer number of pence or a money object; "price: money" closes it.
A single character in front of each member marks its visibility:
| Marker | Visibility | Meaning |
|---|---|---|
| + | Public | Any other class can use it |
| - | Private | Only the class itself can use it |
| # | Protected | The class and its subclasses can use it |
| ~ | Package | Classes in the same package or module can use it |
Two further conventions are worth knowing. A static member, one that belongs to the class rather than to each instance, is underlined. An abstract class or operation is written in italics, or marked with a stereotype such as «abstract» above the name. An interface is a class box with «interface» above the name and, usually, only operations.
A common pattern is private attributes and public operations. If a diagram shows every attribute as public, either the design genuinely exposes its internals or the author did not think about it. Both are worth a question in review.
The five relationships and their notation
Lines between classes carry most of the meaning in a class diagram, and each relationship has a specific end marker. Getting the marker on the wrong end reverses the meaning, so this is the part to learn exactly.
| Relationship | Line | Marker and where it goes | Reads as |
|---|---|---|---|
| Association | Solid | None, or an open arrowhead for navigability | A Customer places Orders |
| Inheritance | Solid | Hollow triangle at the parent class | An Admin is a User |
| Dependency | Dashed | Open arrowhead at the class being used | A ReportBuilder uses a Formatter |
| Aggregation | Solid | Hollow diamond at the whole | A Team has Players, who exist without it |
| Composition | Solid | Filled diamond at the whole | An Order owns its OrderLines, which die with it |
Association
A plain solid line says that instances of one class know about instances of another. It is the default, and the right choice more often than people expect. Label it with a verb phrase (places, is for, refers to) so the line reads as a sentence from one box to the other.
Inheritance
Also called generalisation. The line runs from the subclass to the parent and ends in a hollow triangle touching the parent. The subclass gets everything the parent has and adds or overrides some of it. Use it only when the subclass really is a special case of the parent in every context; if "is a" only holds sometimes, an association or a role is usually the better model.
Dependency
A dashed line with an open arrowhead pointing at the class being used. It is the weakest relationship: one class uses another briefly, typically as a parameter or a local variable, without holding on to it. Many diagrams leave dependencies out entirely unless they explain something important, such as a module boundary being crossed.
Aggregation and composition
Both describe a whole made of parts, and both put the diamond at the whole, not at the part. The difference is lifetime. With composition, the filled diamond, the parts belong to exactly one whole and are created and destroyed with it: delete an order and its order lines go too. With aggregation, the hollow diamond, the parts have their own existence: a role carries on existing when an admin who holds it is removed.
If you are unsure whether something is aggregation or composition, ask what happens to the part when the whole is deleted. If the honest answer is "it depends", draw a plain association and write the rule down somewhere a developer will see it.
Multiplicity: how many on each end
Multiplicity is written near each end of a relationship line and says how many instances can take part. It is the detail that turns a vague diagram into a design someone can implement.
- 1: exactly one. Every OrderLine belongs to exactly one Order.
- 0..1: zero or one. A Loan may or may not have been returned.
- * or 0..*: any number, including none. A Customer may have placed no orders yet.
- 1..*: at least one. An Order must contain at least one OrderLine.
- n..m: a specific range, such as 1..5 for a team of up to five reviewers.
Read multiplicity from the far end. On the line between Customer and Order, the "*" next to Order says one customer has any number of orders; the "1" next to Customer says each order has exactly one customer. Putting both numbers on the same end is a frequent slip, and it produces a diagram that is technically meaningless.
The difference between 0..* and 1..* matters more than it looks. It decides whether an empty collection is valid, which decides whether code has to handle it, which decides whether a database column can be null.
How much detail to show
There is no single right level. The right level is whatever the reader needs for the decision in front of them. Three levels cover most uses:
| Level | Show | Leave out | Good for |
|---|---|---|---|
| Conceptual | Class names and relationships with labels | Attributes, types, operations | Agreeing the domain with non-developers |
| Design | Key attributes with types, important operations, multiplicity | Getters, setters, framework fields | Design reviews and onboarding |
| Implementation | Every member with visibility and full signatures | Very little | Generated documentation; rarely drawn by hand |
Most hand-drawn diagrams should sit at the design level. Some practical rules for staying there:
- Leave out getters and setters. They follow from the attributes.
- Leave out id, createdAt and updatedAt unless they carry meaning in the discussion.
- Show an operation when its name tells the reader something about responsibility, such as Order.total() or Loan.isOverdue().
- Keep one diagram to roughly the number of classes you can hold in your head at once. If it needs scrolling, split it by area.
- Prefer a correct association to a speculative composition. Wrong precision is worse than honest vagueness.
Worked example: a library lending system
A small town library wants software to track loans. The brief from the librarian is short: members borrow books, books are due back in three weeks, and the library wants to know which books are overdue. Here is how that becomes a class diagram.
Step one is to pull out the nouns: member, book, loan, due date. The first draft usually has Member and Book joined by a "borrows" association. Ask the librarian one question, "if you own three copies of the same novel, can three people borrow it at once?", and the model changes. A member does not borrow a title; they borrow a physical item with a barcode. Book (the work, with ISBN, title and author) and Copy (the item, with barcode and condition) need to be separate classes.
Step two is to find the relationship that carries data. The borrowing itself has a date out, a due date and, eventually, a return date. Data that belongs to the relationship rather than to either end is a sign that the relationship should be a class. That is Loan.
Step three is multiplicity. A member has 0..* loans. Each loan is for exactly 1 copy. A copy has 0..* loans over its life but at most one open loan at a time, which is a rule worth writing as a note. Each copy is a copy of exactly 1 book, and a book has 1..* copies if the library only records titles it owns.
Step four is behaviour. The librarian's question was about overdue books, so Loan gets isOverdue(). Member gets borrow() and return(). Nothing else needs an operation yet.
Notice what the example does not use. There is no inheritance and no composition. Deleting a member should not delete the history of their loans, and a copy is not destroyed when a loan ends, so neither diamond fits. A class diagram with only associations is common and perfectly correct.
In DiagramDock, each class is written as plain text in its box: the name on the first line, then a line of dashes before the attributes and another before the operations. The Loan class from the example looks like this:
Loan
---
-borrowedAt: Date
-dueAt: Date
-returnedAt: Date
---
+isOverdue(today: Date): boolean
+close(returnedOn: Date): voidAn interface takes an extra first line with the stereotype:
«interface»
Lendable
---
---
+canLend(): booleanCommon mistakes and how to spot them
- Diamonds at the wrong end. The diamond always touches the whole. If it touches OrderLine, the diagram says the line owns the order.
- Inheritance used for roles. Customer and Staff extending Person looks tidy until one person is both. If a thing can change type or hold two at once, model the role as an association.
- Composition everywhere. Filled diamonds are tempting because they look decisive. Each one is a claim about deletion; check it.
- Plural class names. A class describes one instance. Orders as a class name usually means the author was thinking of a table.
- Missing multiplicity on the lines that matter. You can omit it on obvious lines, but never on a line where 0 versus 1 changes the code.
- Database columns drawn as attributes. customerId on Order duplicates the association line. In a class diagram, the line is the reference.
Drawing class diagrams in DiagramDock
DiagramDock has a UML class diagram type with a class shape that reads the text format above and draws the compartments for you. When you connect two classes, the connector offers the five relationships as one-click choices, with the correct markers: hollow triangle for inheritance, dashed line with open arrow for dependency, hollow and filled diamonds for aggregation and composition. Multiplicity goes in the connector label.
Three class diagram templates (the basic relationships, an ecommerce model and the library system) are available as starting points. Diagrams are stored in your browser rather than on a server, and class diagrams export to SVG, PNG, PDF or Mermaid class syntax if you want the model next to your code.
Questions
- What is the difference between aggregation and composition in UML?
- Both mean one class is made up of others, and both put the diamond at the whole. Composition (filled diamond) means the parts cannot exist without the whole and are destroyed with it. Aggregation (hollow diamond) means the parts have their own lifetime and can exist independently or be shared.
- Which way does the inheritance arrow point in a class diagram?
- From the subclass to the parent class. The hollow triangle sits at the parent, so the arrow reads as "Admin is a User".
- What do +, -, # and ~ mean in a UML class?
- They are visibility markers. + is public, - is private, # is protected (visible to subclasses) and ~ is package visibility. They go in front of an attribute or operation name.
- Do I need to show every attribute and method?
- No. Show what the reader needs for the decision the diagram supports. A design discussion usually needs names, key attributes and relationships; getters, setters and framework fields are noise.
- Is a class diagram the same as an ER diagram?
- They overlap but answer different questions. A class diagram describes objects in code, including behaviour and inheritance. An ER diagram describes stored data, including keys and how tables reference each other.
Keep reading
Technical diagrams9 min read
ER diagrams and database tables: understanding the connection
How entity-relationship diagrams map onto real tables: entities and attributes, primary and foreign keys, join tables and cardinality, with SQL alongside.
Technical diagrams9 min read
Sequence diagrams: seeing what happens between systems
Sequence diagram notation explained: participants, lifelines, messages, returns, activation and notes, a worked login example, and when to prefer one to a flowchart.
Technical diagrams9 min read
Visualising a database before writing the schema
A design-first workflow for databases: nouns to entities, relationships before columns, many-to-many found early, then the diagram turned into CREATE TABLE.