Loading content...
Before a single line of code is written, experienced software engineers envision structure. They see entities, their properties, their behaviors, and how they relate. But imagination alone isn't enough—teams need a way to externalize these mental models, to make them visible, discussable, and refinable.
This is precisely what class diagrams provide: a standardized, visual vocabulary for representing the static structure of object-oriented systems. Among all UML diagram types, class diagrams are the most widely used, the most universally understood, and the most directly translatable to code. If you master one UML diagram type, let it be this one.
By the end of this page, you will understand the three-compartment structure of UML classes, master the notation for class names, attributes, and methods, and be able to read and write class definitions that precisely communicate design intent to any developer familiar with UML.
Before diving into syntax, let's establish why class diagrams deserve such prominence in software design. Understanding their purpose will help you use them effectively rather than merely mechanically.
The fundamental problem class diagrams solve:
Software systems contain numerous entities with complex relationships. Keeping track of:
...quickly exceeds the capacity of informal communication or prose documentation. Class diagrams encode this information in a dense, precise, visually scannable format.
Class diagrams are thinking tools as much as documentation. The act of drawing forces you to make decisions: What exists? What belongs where? What knows about what? This discipline catches design flaws before they become code defects.
Every UML class is represented as a rectangle divided into three horizontal compartments. This structure is sacred in UML—it's the core visual pattern you'll recognize in every class diagram:
┌─────────────────────────────┐
│ ClassName │ ← Name Compartment
├─────────────────────────────┤
│ - attribute1: Type │ ← Attributes Compartment
│ - attribute2: Type │
├─────────────────────────────┤
│ + method1(): ReturnType │ ← Operations Compartment
│ + method2(param): Type │
└─────────────────────────────┘
Let's examine each compartment in detail.
Customer, not Customers) representing the concept.Compartments can be hidden if not relevant to the current discussion. A class might appear as just a rectangle with a name (showing only what exists), or with attributes but no methods (focusing on data structure). UML allows flexible detail levels based on communication needs.
Visual example with a concrete class:
┌─────────────────────────────────────┐
│ BankAccount │
├─────────────────────────────────────┤
│ - accountNumber: String │
│ - balance: Decimal │
│ - owner: Customer │
│ - createdAt: DateTime │
├─────────────────────────────────────┤
│ + deposit(amount: Decimal): void │
│ + withdraw(amount: Decimal): Bool │
│ + getBalance(): Decimal │
│ + transferTo(target: BankAccount, │
│ amount: Decimal): Bool│
└─────────────────────────────────────┘
This single diagram communicates the entire public contract of a BankAccount class. Anyone reading it understands: what data exists, what operations are possible, and the types involved. This is remarkably efficient communication.
The name compartment is deceptively simple—it's just the class name. But naming carries significant meaning and conventions in UML that experienced engineers expect.
OrderProcessor, PaymentGateway, UserAuthentication.Invoice not Invoices, Customer not CustomerList.ShoppingCart not ItemListHolder.Stereotypes and special notations:
The name compartment can include additional information using UML's stereotype notation—keywords in guillemets (« ») placed above the class name:
┌─────────────────────────┐
│ «interface» │
│ Payable │
├─────────────────────────┤
│ │
├─────────────────────────┤
│ + processPayment() │
└─────────────────────────┘
┌─────────────────────────┐
│ «abstract» │
│ Vehicle │
├─────────────────────────┤
│ # speed: Integer │
├─────────────────────────┤
│ + accelerate() │
│ + brake() │
└─────────────────────────┘
┌─────────────────────────┐
│ «enumeration» │
│ OrderStatus │
├─────────────────────────┤
│ PENDING │
│ PROCESSING │
│ SHIPPED │
│ DELIVERED │
│ CANCELLED │
└─────────────────────────┘
| Stereotype | Meaning | Visual Appearance |
|---|---|---|
| «interface» | Defines a contract without implementation | Name in italics or with stereotype label |
| «abstract» | Cannot be instantiated directly; meant for inheritance | Name in italics or with stereotype label |
| «enumeration» | Defines a fixed set of named values | Attributes list the enumeration constants |
| «utility» | Contains only static members (helper class) | All members shown as underlined (static) |
| «entity» | Represents a persistent domain object | Common in domain-driven design diagrams |
| «service» | Represents a stateless service/behavior class | Common in service-oriented architectures |
Abstract classes can be shown either with the «abstract» stereotype or by rendering the class name in italics. Both are valid UML; italics are more compact, while stereotypes are more explicit. Choose based on your audience's familiarity with UML.
The attributes compartment defines the data structure of the class—the properties, fields, or member variables that instances of this class will hold. This is where we specify what information an object encapsulates.
The attribute syntax follows a specific format:
visibility name : type [multiplicity] = defaultValue {property-string}
Let's break down each component:
| Component | Required? | Example | Meaning |
|---|---|---|---|
| visibility | Yes | -, +, #, ~ | Access level (private, public, protected, package) |
| name | Yes | accountNumber | The attribute identifier, in camelCase |
| type | Yes | String, Integer, Customer | The data type of the attribute |
| multiplicity | No | [0..*], [1], [0..1] | How many values (for collections) |
| defaultValue | No | = 0, = "pending" | Initial value if not specified |
| property-string | No | {readOnly}, {ordered} | Constraints or properties |
Practical examples:
- id: UUID // Private UUID identifier
- name: String // Private string
- balance: Decimal = 0.00 // Private with default value
- status: OrderStatus = PENDING // Private enumeration with default
+ email: String // Public string (unusual but valid)
# createdAt: DateTime // Protected timestamp
- items: OrderItem [0..*] // Collection of OrderItems
- shippingAddress: Address [0..1] // Optional single Address
- tags: String [*] {ordered} // Ordered collection of strings
- version: Integer {readOnly} // Read-only (immutable after creation)
Multiplicity notation ([0..*], [1..*], [0..1]) conveys critical information about how many related objects exist. Omitting multiplicity implies exactly one ([1]). Getting this wrong leads to incorrect implementations—a common source of bugs.
[1] or omitted — Exactly one; mandatory and single-valued[0..1] — Zero or one; optional single value (nullable)[*] or [0..*] — Zero or more; optional collection[1..*] — One or more; mandatory collection with at least one[3..5] — Specific range; between 3 and 5 (rare but valid)Derived and static attributes:
Some attributes have special characteristics that UML can express:
- /age: Integer // Derived (calculated from birthDate)
- _instanceCount: Integer // Static (underlined in diagrams)
/) are calculated from other attributes rather than stored directly. They're shown for documentation but signal "don't store this—compute it."_ or use {static}.The operations compartment defines what a class can do—the methods, functions, or procedures that provide the class's behavior. This is where we specify the class's interface to the outside world.
The operation syntax follows this format:
visibility name (parameters) : returnType {property-string}
| Component | Required? | Example | Meaning |
|---|---|---|---|
| visibility | Yes | +, -, #, ~ | Access level for the method |
| name | Yes | calculateTotal | The method identifier, in camelCase |
| parameters | Yes (can be empty) | (amount: Decimal) | Input parameters with types |
| returnType | No (void if omitted) | Decimal, Boolean, void | What the method returns |
| property-string | No | {query}, {abstract} | Constraints or properties |
Parameter syntax:
Parameters follow their own format: direction name : type = defaultValue
+ processOrder(order: Order): Boolean
+ calculateDiscount(amount: Decimal, rate: Decimal = 0.1): Decimal
+ findByStatus(status: OrderStatus, limit: Integer = 100): Order[*]
+ updateAddress(inout address: Address): void
Parameter directions (rarely used but valid in UML):
in — Input only (default if omitted)out — Output only (method populates the parameter)inout — Both input and output (modified by method)Operation properties and stereotypes:
Operations can carry additional metadata:
+ getBalance(): Decimal {query} // Doesn't modify state (getter)
+ calculateHash(): String {abstract} // Must be implemented by subclasses
+ getInstance(): Singleton {static} // Class-level, not instance-level
+ finalize(): void {leaf} // Cannot be overridden
Common operation properties:
{query} — The operation does not modify the object's state; it's a pure reader/getter. This is essential for side-effect-free query methods.{abstract} — The operation has no implementation in this class; subclasses must provide it. Alternatively, show the operation name in italics.{static} — The operation belongs to the class, not instances. Also shown by underlining in visual diagrams.{leaf} — The operation cannot be overridden in subclasses (final/sealed).{redefines operationName} — Explicitly overrides an inherited operation.In complex classes, operations can be organized visually by responsibility: constructor operations first, then queries (getters), then commands (mutators), then factories. This makes diagrams scannable and matches common code organization conventions.
Let's construct a comprehensive example that demonstrates all the notation we've covered. We'll model an Order class that might appear in an e-commerce system:
┌───────────────────────────────────────────────────────────┐
│ Order │
├───────────────────────────────────────────────────────────┤
│ - id: UUID │
│ - orderNumber: String │
│ - customer: Customer │
│ - items: OrderItem [1..*] │
│ - status: OrderStatus = PENDING │
│ - shippingAddress: Address [0..1] │
│ - /totalAmount: Decimal │
│ - createdAt: DateTime │
│ - updatedAt: DateTime │
│ - _orderCount: Integer {static} │
├───────────────────────────────────────────────────────────┤
│ + Order(customer: Customer, items: OrderItem[1..*]) │
│ + addItem(item: OrderItem): void │
│ + removeItem(itemId: UUID): Boolean │
│ + calculateTotal(): Decimal {query} │
│ + applyDiscount(discount: Discount): void │
│ + submit(): Boolean │
│ + cancel(): Boolean │
│ + getStatus(): OrderStatus {query} │
│ + setShippingAddress(address: Address): void │
│ + getOrderCount(): Integer {static, query} │
│ # validateStateTransition(newStatus: OrderStatus): Bool │
│ - notifyCustomer(event: OrderEvent): void │
└───────────────────────────────────────────────────────────┘
Let's decode every element:
Attributes:
- id: UUID — Private unique identifier- orderNumber: String — Private human-readable order number- customer: Customer — Private reference to a Customer object (association)- items: OrderItem [1..*] — Private collection of OrderItems; must have at least one- status: OrderStatus = PENDING — Private enum with default value- shippingAddress: Address [0..1] — Optional (might not be set yet)- /totalAmount: Decimal — Derived; calculated from items, not stored- createdAt: DateTime, - updatedAt: DateTime — Timestamps- _orderCount: Integer {static} — Class-level counter (underlined visually)Operations:
+ Order(...) — Public constructor+ addItem(...), + removeItem(...) — Public mutators for collection+ calculateTotal(): Decimal {query} — Public query (no side effects)+ applyDiscount(...), + submit(), + cancel() — State-changing operations# validateStateTransition(...) — Protected helper (accessible to subclasses)- notifyCustomer(...) — Private internal implementation detailA developer reading this diagram can immediately understand: the data an Order contains, what operations are possible, which operations are public vs. internal, which attributes are mandatory vs. optional, and what types everything uses. This is faster and more precise than reading code or prose.
Knowing the syntax is only half the battle. Let's examine common mistakes and how to avoid them.
string in one place and String in another. Pick a convention and stick to it.[0..*] for collections leads to ambiguity.Customer, OrderStatus) not generic placeholders.Diagrams can have too much detail (overwhelming) or too little (useless). The right amount depends on your audience. For design discussions, show public interfaces and key attributes. For detailed implementation specs, show more.
One of class diagrams' greatest strengths is their direct translatability to code. Let's see how the Order class translates to actual implementations:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
// TypeScript translation of the Order class diagramenum OrderStatus { PENDING = 'PENDING', PROCESSING = 'PROCESSING', SHIPPED = 'SHIPPED', DELIVERED = 'DELIVERED', CANCELLED = 'CANCELLED'} class Order { // Private attributes private id: string; private orderNumber: string; private customer: Customer; private items: OrderItem[]; // [1..*] enforced in constructor private status: OrderStatus = OrderStatus.PENDING; private shippingAddress?: Address; // [0..1] = optional private createdAt: Date; private updatedAt: Date; // Static attribute private static orderCount: number = 0; // Derived attribute as getter get totalAmount(): number { return this.calculateTotal(); } // Constructor constructor(customer: Customer, items: OrderItem[]) { if (items.length === 0) { throw new Error("Order must have at least one item"); } this.id = crypto.randomUUID(); this.orderNumber = this.generateOrderNumber(); this.customer = customer; this.items = [...items]; this.createdAt = new Date(); this.updatedAt = new Date(); Order.orderCount++; } // Public operations public addItem(item: OrderItem): void { this.items.push(item); this.updatedAt = new Date(); } public removeItem(itemId: string): boolean { const index = this.items.findIndex(i => i.id === itemId); if (index === -1) return false; if (this.items.length === 1) return false; // Can't remove last item this.items.splice(index, 1); this.updatedAt = new Date(); return true; } // Query operation (no side effects) public calculateTotal(): number { return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0); } public getStatus(): OrderStatus { return this.status; } // Static operation public static getOrderCount(): number { return Order.orderCount; } // Protected operation protected validateStateTransition(newStatus: OrderStatus): boolean { // State machine logic here return true; } // Private operation private notifyCustomer(event: OrderEvent): void { // Internal notification logic }}Notice how each element in the diagram maps to code: visibility becomes access modifiers, types become type annotations, multiplicities become collection types or optionals. This predictable mapping is what makes class diagrams so valuable—they're essentially language-neutral type definitions.
We've covered the foundation of UML class notation. Let's consolidate the key takeaways:
visibility name : type [multiplicity] = default {properties} captures everything about data.visibility name (parameters) : returnType {properties} captures everything about behavior.+ public, - private, # protected, ~ package. Always include them.[0..1], [1..*], [*] specify how many values an attribute holds—critical for correct implementation.What's next:
Now that you understand how to notate a single class, we'll explore visibility markers in greater depth. Understanding when to use public, private, protected, and package visibility is essential for encapsulation and determines how classes interact in a larger system.
You now have the vocabulary to read and write UML class definitions. In the next page, we'll master visibility markers—the mechanism that controls encapsulation and defines the boundaries of class interfaces.