UML Class Diagrams

types, relationships, and notation, illustrated with TextFinder

1.  What a Class Diagram Shows

A class diagram is the workhorse of structural UML. It shows the types a system defines - their data, their operations, and the relationships between them. Where a package diagram shows compilation boundaries, a class diagram shows what lives inside those boundaries: which types carry which state, what interface each type exposes, and how types depend on, own, or extend each other. Class diagrams are language-independent in intent but language-specific in detail. A "class" in the diagram maps to a struct, a class, a trait, an interface, or an abstract base depending on the language. The diagram does not care which - it expresses the design contract; the language expresses the mechanism.

2.  The Class Box

Each type appears as a rectangle with up to three compartments stacked vertically. The top compartment holds the type name, optionally preceded by a stereotype. The middle compartment lists attributes (fields). The bottom compartment lists operations (methods). Either or both of the lower compartments may be omitted when the detail is not relevant to the diagram's purpose. Visibility prefixes on members follow a consistent convention across all languages:
PrefixVisibilityMeaning
+ public Accessible to any caller.
- private Accessible only within the type itself.
# protected Accessible within the type and its subclasses.
~ package Accessible within the enclosing package (Java/C# internal).
Stereotypes label the kind of type: <<interface>>, <<abstract>>, <<trait>>, <<enum>>. An abstract operation is shown in italics; Mermaid approximates this with an asterisk suffix.

3.  Relationships

Relationships are the most important part of a class diagram - the boxes alone say little. Each relationship type carries a distinct meaning about how one type depends on or contains another.
classDiagram
    direction LR
    class Owner
    class Part
    class Whole
    class Member
    class IFace {
        <<interface>>
    }
    class Concrete
    class User
    class Target
    class Child
    class Parent

    Owner --* Part     : composition - Owner owns Part lifetime
    Whole --o Member   : aggregation - Member outlives Whole
    Concrete ..|> IFace : realization - Concrete implements IFace
    Child --|> Parent   : inheritance - Child is-a Parent
    User --> Target     : association - User holds a reference to Target
      

Figure 1. The five principal relationship types.

Arrow direction always reads "source depends on target."
RelationshipLineMeaning
Composition solid, filled diamond at owner Owner controls the lifetime of the part. The part cannot exist independently - when the owner is destroyed, the part is destroyed with it. Use for data members owned by value or by unique pointer.
Aggregation solid, hollow diamond at whole A "has-a" relationship where the member has an independent lifetime. The whole holds a reference or shared pointer; destruction of the whole does not destroy the member.
Realization dashed, hollow triangle at interface A type fulfills the contract declared by an interface or trait. All abstract operations in the interface must be provided by the concrete type.
Inheritance solid, hollow triangle at parent The child type is substitutable for the parent. It inherits data and operations, and may override virtual ones. Absent in Rust; present in C++, C#, and Python.
Association solid arrow One type holds a reference to another without owning its lifetime. The most general structural link - use it when none of the stronger relationships apply.
Dependency dashed arrow One type uses another transiently - as a function parameter, a local variable, or a return value - but does not store a reference to it. The weakest structural link.

4.  TextFinder as a Worked Example

The Rust TextFinder implementation provides a clean class diagram subject because it uses a trait (DirEvent) to decouple directory traversal from the application logic. The five types, their members, and their relationships are shown below. The C++, C#, and Python implementations express the same design with language-specific mechanisms.
classDiagram
    class DirEvent {
        <<trait>>
        +do_dir(d: &str)*
        +do_file(f: &str)*
    }
    class TfAppl {
        -curr_dir: String
        -hide: bool
        -tf: TextFinder
        +new(cl: &CmdLine) TfAppl
        +do_dir(d: &str)
        +do_file(f: &str)
        +get_hide() bool
    }
    class DirNav {
        -skip_dirs: Vec~String~
        -patterns: Vec~String~
        +new() DirNav
        +add_pattern(p: &str)
        +search(path: &str, app: &mut dyn DirEvent)
    }
    class TextFinder {
        -regex: Option~Regex~
        -last_path: String
        +new() TextFinder
        +regex(pattern: &str)
        +find(fqf: &str) bool
        +get_last_path() ~&str~
        +last_path(p: &str)
    }
    class CmdLine {
        -options: HashMap~String, Vec~String~~
        +new() CmdLine
        +parse(args: Vec~String~)
        +get(key: &str) Option~&str~
        +get_all(key: &str) Vec~&str~
    }

    DirEvent <|.. TfAppl      : implements
    DirNav --> DirEvent        : calls via trait object
    TfAppl *-- TextFinder     : owns
    TfAppl --> CmdLine        : reads options from
      

Figure 2. Rust TextFinder class diagram.

TfAppl is the only type that connects all four others - it implements the DirEvent trait, owns a TextFinder, and reads from CmdLine. DirNav never sees the concrete type, only the trait.

5.  Reading the Diagram

Start at the relationships, not the boxes. Four arrows leave or arrive at TfAppl: The class diagram does not show EntryPoint because it contains no persistent state - it is a wiring function, not a type. That is a design choice: types with only procedural logic often do not need class boxes.

6.  Cross-Language Type Mapping

The class diagram is language-neutral. The same five-type structure appears in all four TextFinder languages, with each language translating the trait pattern into its own mechanism.
Concept Rust C++ C# Python
Callback contract trait DirEvent abstract base class or std::function interface IDirEvent or Action<> callable protocol or ABC
Realization impl DirEvent for TfAppl override virtual methods class TfAppl : IDirEvent implement do_dir, do_file
Ownership of TextFinder value field in struct value member or unique_ptr field (reference type, GC-managed) instance attribute
Dispatch to TextFinder direct method call direct method call direct method call direct method call
The realization relationship is the one that varies most. Rust requires an explicit impl block; C++ uses virtual function override; C# uses interface declaration syntax; Python relies on duck typing and optionally an abstract base class. All four express the same class diagram relationship: a concrete type fulfills a declared contract.

7.  When to Draw One

Draw a class diagram when the question involves what types exist and how they relate, not where they live or what order events occur in. Specific triggers: Avoid drawing every class in a large codebase. Class diagrams become unreadable past about a dozen types. Show only the types relevant to the decision or question at hand, and label the diagram accordingly.

8.  References

ResourceDescription
Project Story: TextFinder Architecture, CLI, performance, and code metrics for all five implementations.
UML Package Diagrams The companion page showing compilation-boundary structure - the level above class diagrams.
Mermaid class diagram syntax Full syntax reference for Mermaid classDiagram, including multiplicity, notes, and styling.
UML Class Diagrams Overview Formal UML notation reference covering all relationship types, multiplicities, and constraints.