Most UML diagram types live in textbooks. The class diagram is the one engineers still draw: a picture of the things a system is about, what each of them holds, what it can do, and how they relate. Drawn well, it settles arguments about the domain model before anyone writes a migration or an API.
This post builds a UML class diagram for a small online course platform, one decision at a time, and explains the notation as it comes up: visibility, multiplicity, association, composition, inheritance and interfaces. It ends with the mistakes that make class diagrams useless and a short look at how the diagram maps to code.
When a class diagram is the right picture
The UML 2.5.1 specification splits its diagrams into structure diagrams and behavior diagrams. A class diagram is a structure diagram: it shows types, their attributes and operations, and the relationships between them, with no notion of time.
| Question you need answered | Diagram |
|---|---|
| Which concepts exist, what they know, how they relate | Class diagram |
| How the data is stored: tables, keys, foreign keys | ER diagram |
| What happens in what order between participants | Sequence diagram |
| Which states one object moves through | State machine diagram |
A class diagram earns its place for a domain model, for the public types of a library, or for a design you are about to build. It does not earn its place as a dump of every class in a repository: an IDE shows that better, and the picture is out of date after the next commit.
The notation in two tables
A class is a box with up to three compartments: the name, the attributes and the operations. Any compartment can be left out.
| Element | Notation | Meaning |
|---|---|---|
| Abstract class | Name in italics, or {abstract} |
Cannot be instantiated; subclasses complete it |
| Interface | «interface» above the name |
A contract of operations |
| Visibility | + public, - private, # protected, ~ package |
Who may use the feature |
| Attribute | name: Type [multiplicity] = default |
Data the object holds |
| Operation | name(param: Type): ReturnType |
Behavior the object offers |
| Static feature | Underlined | Belongs to the class, not to an instance |
Relationships are lines between classes, and the ends carry most of the meaning:
| Relationship | Line | Read it as |
|---|---|---|
| Association | Solid line; an open arrowhead marks a navigable end | "is related to" |
| Aggregation | Solid line, hollow diamond at the whole | "groups"; the parts live on their own |
| Composition | Solid line, filled diamond at the whole | "is made of"; the parts go with the whole |
| Generalization | Solid line, hollow triangle at the parent | "is a kind of" |
| Realization | Dashed line, hollow triangle at the interface | "implements" |
| Dependency | Dashed line, open arrowhead at the target | "uses"; a change to the target can break the source |
Composition is the strict one. The specification says a part object belongs to at most one composite at a time, and when the composite is deleted, its parts are deleted with it. Shared aggregation, the hollow diamond, is the vague one: the specification itself says its precise semantics vary by application area and modeler. Use it only if your team agrees what it means; a plain association says the same thing with less debate.
Multiplicity
Multiplicity says how many objects can sit at one end of an association: 1 for exactly one, 0..1 for optional, * (the same as 0..*) for any number, 1..* for at least one, and ranges such as 2..5. Write it at the end next to the class it counts. Read Course 1 — 1..* Module as "a course has one or more modules, and each module belongs to exactly one course."
Missing multiplicities are the most common reason a class diagram starts arguments instead of ending them. Every association in the example below has both ends filled in.
The example: an online course platform
Start from plain sentences, not boxes:
- Instructors publish courses. A course has one or more instructors.
- A course is made of ordered modules, and a module of ordered lessons.
- A lesson is a video, a quiz or an assignment.
- Quizzes and assignments are graded; videos are not.
- Students enroll in courses. An enrollment tracks progress and holds the student's submissions.
- A student who completes a course gets one certificate for that enrollment.
- Courses are grouped into categories, and a course can be in several.
Users: one class, two roles
The first instinct is Student and Instructor as subclasses of User. That breaks the first time an instructor takes someone else's course: in most programming languages an object has one class for its whole life, so it cannot be both, or switch later. Model roles as associations instead: a User teaches courses, and a User holds enrollments. Keep inheritance for things that are a kind of something for their whole life, such as a quiz being a kind of lesson.
Composition where lifecycles are tied
A module means nothing outside its course, and a lesson means nothing outside its module. Deleting a course deletes both. That is composition: a filled diamond at Course and at Module, with 1 at the whole. Categories are different. Deleting the "Data engineering" category must not delete its courses, so Category to Course is aggregation with * on both ends.
Inheritance plus an interface
Lesson is abstract; VideoLesson, Quiz and Assignment inherit its title and position. Grading cuts across that tree: quizzes and assignments are graded, videos are not, and a peer review added next year might be graded without being a lesson at all. That is an interface, Gradable, realized by Quiz and Assignment. GradingService depends on Gradable, not on the concrete classes, and the dashed dependency arrow says exactly that.
An association with data of its own
Enrollment sits between User and Course. UML has a dedicated notation for this, the association class, attached to the line by a dashed line. Most teams draw it as an ordinary class with two associations instead, because it has its own identity, data and behavior: the enrollment date, progress, submissions and at most one certificate.
Diagram contents
- Category
- Course
- User
- Module
- Enrollment
- Certificate
- Lesson
- Submission
- VideoLesson
- Quiz
- Assignment
- Gradable
- GradingService
- groups
- teaches
- enrolls
- for
- earns
- holds
- answers
- «use»
How to read it:
- Top left, the catalog.
Courseis composed ofModule, which is composed ofLesson, with filled diamonds at the whole.Categorygroups courses with a hollow diamond. - Bottom left, lesson types.
Lessonis abstract.VideoLesson,QuizandAssignmentpoint to it with hollow triangles. - Right, learning. A
Userteaches courses, with1..*instructors per course, and has any number of enrollments, each for exactly one course. AnEnrollmentholds submissions and earns at most oneCertificate. - Bottom right, grading.
QuizandAssignmentrealizeGradablewith dashed lines and hollow triangles. EachSubmissionanswers one gradable item, andGradingServicedepends on the interface only.
Common mistakes
- Every class in the codebase. Ten to fifteen classes per diagram is a good limit. Split a larger model by area, or show the rest as names without compartments.
- Getters and setters as operations. List operations that carry domain meaning, such as
publish()orenroll(), not accessors. - Tables drawn as classes. Attributes like
courseIdbelong in the ER diagram. In a class diagram, the association line is the reference. - Inheritance for roles. If an object can gain or lose the role, it is an association, not a subclass.
- Diamonds without meaning. If nobody can say what deleting the whole does to the parts, use a plain association.
- Implementation detail at domain level. Leave out framework annotations, DTOs and repositories unless the diagram is about them.
From the diagram to code
The diagram maps to code directly. In TypeScript:
interface Gradable {
maxScore(): number;
autoGrade(submission: Submission): Score | null;
}
abstract class Lesson {
constructor(public title: string, public position: number) {}
}
class Quiz extends Lesson implements Gradable {
questions: Question[] = [];
maxScore(): number {
return this.questions.length;
}
autoGrade(submission: Submission): Score | null {
return submission.scoreAgainst(this.questions);
}
}
class Course {
private readonly modules: Module[] = []; // composition: owned and removed with the course
categories: Category[] = []; // aggregation: categories exist on their own
addModule(title: string): Module {
const created = new Module(title, this.modules.length);
this.modules.push(created);
return created;
}
}
Composition becomes ownership: the course creates its modules, and nothing keeps one alive after the course is gone. Aggregation becomes a reference to an object with its own lifecycle. When the code and the diagram disagree, update the diagram in the same pull request; that is far easier when the diagram is a link rather than a screenshot, as described in architecture diagrams that don't rot.
Build it in Archboard
- Sign in with Google or GitHub and create a diagram.
- From the UML pack, add a Class for each concept and write attributes and operations one per line. Set the stereotype of
Lessontoabstract. - Add an Interface for
Gradablewith its two operations. - Connect the classes. Set the arrow end at the whole to Filled diamond for composition and Diamond for aggregation, and to Triangle at the parent for inheritance. Draw realization as a dashed line with a Triangle at the interface, and dependency as a dashed line with an Open arrow.
- Put multiplicities next to each end as text, and name associations such as "teaches" in the edge label.
- Add a UML Note for decisions, such as why roles are associations.
The example uses 13 diagram nodes, within the Free plan. With an AI agent connected over MCP, you can describe the requirements and let it draft the classes, then fix the multiplicities yourself.
FAQ
What is the difference between a class diagram and an object diagram? A class diagram shows types and the rules between them. An object diagram shows specific instances at one moment, with their values and links, and is useful for checking a class diagram against a real example.
What is the difference between aggregation and composition? With composition, a part belongs to one whole and is deleted with it. With aggregation, the parts can exist on their own and be shared; its exact meaning is left to the modeler, so many teams use a plain association instead.
Should a class diagram match the code exactly? A domain model should match the concepts and relationships, not every field and method. Keep it at the level where a product manager and an engineer can both read it.
How is a class diagram different from a C4 component diagram? A C4 component diagram shows deployable building blocks and their responsibilities. A class diagram goes one level deeper, to the types inside them; see the C4 model diagram example.
Can I draw a class diagram from code automatically? Many IDEs can, but the result shows every class and is hard to read. Draw the domain model by hand or with an AI agent, and keep it to the classes that matter.