UML
What is UML?
UML, short for Unified Modeling Language, is a notation for describing the structure and behavior of software. A diagram can show relationships between types, messages exchanged by objects, or the conditions under which a state changes.1
UML provides notation for describing structure and behavior. The same relationships can be drawn by hand, in a diagram editor, or with a text-based tool. The choice of programming language, development method, and drawing tool is separate.
Why use it?
Consider a small diagnostic reporter. The error data stays the same, but producing a readable sentence and producing JSON require different formatting. A formatter is the component that converts data into one of these representations.
If the reporter knows every formatting rule, each new format requires a change to the reporter. Instead, the reporter can depend on a shared contract and delegate the conversion to a supplied formatter. Here, the contract is a calling rule: accept a diagnostic and return a string.
A UML diagram helps us see who references whom and what changes when we add a format, before reading all the code. It gives reviewers a shared picture to discuss. Code review and tests then check whether the implementation follows that design.
Choose a diagram for the question
Question | Useful diagram |
|---|---|
What are the types, their responsibilities, and their relationships? | Class diagram |
Who calls whom, and in what order, in one scenario? | Sequence diagram |
Which events are allowed in a state, and what changes next? | State machine diagram |
Where does a workflow branch, repeat, or run in parallel? | Activity diagram |
How do larger software components connect? | Component diagram |
Where are artifacts deployed across devices and execution environments? | Deployment diagram |
- Question
What are the types, their responsibilities, and their relationships?
- Useful diagram
Class diagram
- Question
Who calls whom, and in what order, in one scenario?
- Useful diagram
Sequence diagram
- Question
Which events are allowed in a state, and what changes next?
- Useful diagram
State machine diagram
- Question
Where does a workflow branch, repeat, or run in parallel?
- Useful diagram
Activity diagram
- Question
How do larger software components connect?
- Useful diagram
Component diagram
- Question
Where are artifacts deployed across devices and execution environments?
- Useful diagram
Deployment diagram
Pattern documents often include class, sequence, and state machine diagrams. To read such a diagram, you first need to know what its boxes and lines each stand for.
Read a class box from top to bottom
DiagnosticReporteris the type name.- formatter: IDiagnosticFormatteris a private property named formatter. Its type follows the colon.+ report(d: Diagnostic): Stringis a public operation that accepts d of type Diagnostic and returns a string. An implementation usually represents it as a method.
Before a member, + means public, - means private, # means protected, and ~ means package visibility. A package in UML is a namespace that groups elements together, and it is a mistake to read it as unconditionally the same thing as a directory or a language's internal visibility.
The keyword «interface» identifies an interface. Abstract types and operations may be italicized; static features are underlined. A diagram selects the compartments and members relevant to its purpose. Check the code for the complete definition.2
Read the line and its endpoint together
Generalization: a solid line with a hollow triangle points from a more specific type to a more general type. Class inheritance is a common example.
Realization: a dashed line with a hollow triangle points from an implementation to its contract. It can show that a class implements an interface.
Dependency: a dashed line with an open arrow points from a client to the supplier it needs. Using a type as a method parameter is one example.
Association: a solid line represents a relationship between instances. An open arrowhead marks a navigable end. Storing an object reference in a field is one way to implement such a relationship.
The triangle points to the general type or contract. Think of extends or implements to read its direction. Use arrowheads or the legend to establish an association's navigability. When the diagram leaves that direction unspecified, check the model definition or code for the references.
A hollow diamond marks shared aggregation; a filled diamond marks composition. Both place the diamond at the whole end. Shared aggregation identifies a whole-part relationship, but its precise ownership and lifetime rules must be defined by the model.3
In a composition, a part belongs to at most one composite whole at a time. Deleting the whole also deletes its parts in the model; a part detached beforehand may survive. The implementation must provide the memory cleanup or database deletion needed to enforce those lifetime rules.
Read the formatter example as a whole
The dashed line and hollow triangle from
HumanFormattertoIDiagnosticFormattershow that the readable-text formatter implements the shared contract.The solid line from
DiagnosticReportertoIDiagnosticFormattershows that the reporter references its formatter. The role name formatter identifies that property.The dashed arrow from
DiagnosticReportertoDiagnosticshows that report uses the diagnostic passed to it. A field or association would show a stored reference.
Numbers at the ends of an association are multiplicities. For each reporter, 1 at the formatter end means exactly one formatter. In the other direction, 0..* means that zero or more reporters may reference a formatter. Multiple reporters are allowed to share the same formatter.
0..1 means zero or one; 1..* means one or more. Read the number as the permitted count at this end for one instance at the opposite end. When an association diagram omits the number, consult the model definition for its multiplicity.4
The same structure in code
These two programs have the same structure and output. The diagram omits constructors and shows the formatter property on the association. String denotes a string type; {readOnly} marks a property that cannot be directly changed after initialization. Operation and property names follow TypeScript camelCase; the C# code uses Format, Report, and Message according to its naming conventions.
using System;
// Keep the data separate from its display format.
public sealed record Diagnostic(string Message);
// This is the contract that a reporter can call.
public interface IDiagnosticFormatter
{
string Format(Diagnostic d);
}
public sealed class HumanFormatter : IDiagnosticFormatter
{
public string Format(Diagnostic d) => "Error: " + d.Message;
}
public sealed class DiagnosticReporter
{
private readonly IDiagnosticFormatter formatter;
// Store the supplied formatter instead of constructing one here.
public DiagnosticReporter(IDiagnosticFormatter formatter)
{
this.formatter = formatter;
}
// Delegate formatting and return the resulting text.
public string Report(Diagnostic d) => formatter.Format(d);
}
public static class Program
{
public static void Main()
{
var reporter = new DiagnosticReporter(new HumanFormatter());
Console.WriteLine(reporter.Report(new Diagnostic("Name not found.")));
}
}// Keep the data separate from its display format.
class Diagnostic {
constructor(public readonly message: string) {}
}
// This is the contract that a reporter can call.
interface IDiagnosticFormatter {
format(d: Diagnostic): string;
}
class HumanFormatter implements IDiagnosticFormatter {
format(d: Diagnostic): string {
return "Error: " + d.message;
}
}
class DiagnosticReporter {
// Store the supplied formatter instead of constructing one here.
constructor(private readonly formatter: IDiagnosticFormatter) {}
// Delegate formatting and return the resulting text.
report(d: Diagnostic): string {
return this.formatter.format(d);
}
}
const reporter = new DiagnosticReporter(new HumanFormatter());
console.log(reporter.report(new Diagnostic("Name not found.")));Both programs produce the same output.
Error: Name not found.Rather than creating a formatter itself, the reporter keeps the one the caller passes to its constructor and delegates the conversion to it. Adding a class for another format leaves the reporter's Report or report method unchanged. The Strategy pattern develops this separation in more detail.
A sequence diagram follows one execution
Now consider a single call to report in the same example. The class diagram showed relationships between types, while this one shows the calls and replies between the objects that actually take part.
The boxes at the top name participants.
reporter: DiagnosticReporteridentifies an object named reporter of type DiagnosticReporter.The dashed line that runs down from the box is the lifeline, drawn along the object that takes part in that execution.
Here, a solid line with a filled arrowhead is a synchronous call: its caller waits for it to return. A dashed line with an open arrowhead is a reply.
A narrow rectangle sitting on a lifeline marks an execution. The stretch in which reporter hands the conversion to human sits inside the execution of reporter's own call.
Follow the calls and replies from top to bottom. Read the order of events on each lifeline together with the sending and receiving of messages; independent events on separate lifelines follow their own local order. Treat vertical spacing as layout, and use explicit time values or constraints to read elapsed time.5
A state machine diagram shows allowed changes
Consider a music player. Pressing play while stopped starts the music; pausing during playback keeps the current position. We will model three states—Stopped, Playing, and Paused—and draw where each button press takes the player.
The small filled circle marks the initial entry. This player starts in
Stopped.Rounded boxes represent states:
Stopped,Playing, andPaused.An arrow is a transition.
playmoves Stopped to Playing;pausemoves Playing to Paused.resumecontinues playback, andstopreturns either Playing or Paused to Stopped.
A transition can be labeled event [guard] / effect. Labeling the arrow from Stopped to Playing play [trackLoaded] / startAudio() means that a play event starts the audio and enters Playing when a track is loaded. Add guards and effects where the model needs them.6
A state diagram is a picture of the behavior rules, while the State pattern is a way of organizing code that puts state-specific behavior in separate objects. That does not mean the diagram has to be implemented with the State pattern, since a small model is well served by an enum and conditionals.
When drawing your own
Settle on one question first, because “who references whom” and “in what order are the calls made” ask for different diagrams.
Include only the types and objects that the question needs, and there is no need to list every constructor and helper method.
Check the meaning of solid lines, dashed lines, triangles, and diamonds, then add the necessary role names and multiplicities. Explain any nonstandard notation in a legend.
Compare the diagram with the code. Is a reference stored, is a type used only as a parameter, or is an interface implemented? Update explanatory diagrams when the code changes.
Example environment: C# was run on .NET 10; TypeScript was compiled for ES2022 and run with Node.js.
Related reading
Strategy pattern: interfaces and interchangeable implementations
State pattern: state-specific behavior and transitions
OMG UML 2.5.1: specification and version record
Footnotes
- OMG, What is UML?, purpose and scope. The detailed notation in this article follows the UML 2.5.1 specification. ↩
- OMG UML 2.5.1, §9.2.4 Classifier notation, §9.4.4 Feature notation, §9.5.4 Property notation. ↩
- OMG UML 2.5.1, §7.7.4 Dependencies, §9.2.4.2 Generalization, §9.5.3 AggregationKind, §11.5.4 Associations. ↩
- OMG UML 2.5.1, §7.5.4 Multiplicity notation, §11.5.4 Associations. Omission on an association diagram differs from defaults in textual property notation. ↩
- OMG UML 2.5.1, §17.1.3 Partial ordering, §17.3.4 Lifelines, §17.4.4.1 Messages. ↩
- OMG UML 2.5.1, §14.2.4 States, Pseudostates, Transitions. ↩