Diagrams as Code

Object-Oriented Design with UML

Course 1 · Chapter 2 · Diagrams as Code: Mermaid and PlantUML

Chapter 1 argued for sketches that are cheap to change. A diagram you draw with a mouse in a drawing tool is cheap to make and expensive to change: nobody can see what changed between two versions, two people cannot edit it at once, and it quietly rots in a folder nobody opens. A diagram written as text behaves like code: it goes in the repository, shows up in a pull request, and is regenerated by a command. This chapter sets up the two tools the course uses, for real, and shows where each is strong.

What was run, and what was not
Everything on this page was run on Windows with these versions: Mermaid 12.1.0 drawn by @mermaid-js/mermaid-cli 12.0.0 (using the installed Chrome), and PlantUML 1.2026.8 (released 2026-09-05), the native Windows build from the project's GitHub releases. Both install into a scratch folder, nothing system-wide. The diagrams you see are the tools' real output. Not checked: that the PlantUML download is authentic beyond coming from the project's release page (its release lists signature files for the jar files, and I did not verify any); how the tools behave on macOS or Linux; and which editors preview which format (I did not test VS Code or JetBrains extensions). Where the page says "GitHub renders Mermaid" that comes from GitHub's own documentation, which I read.

Why Text Beats Drag-and-Drop (for Developers)

Diagram as textDrawn in a GUI tool
Reviewing a changeA normal diff, a few lines"Binary file changed", or a screenshot someone must remember to update
Version historyEvery change is a commit with a reasonWhatever the file format and your discipline allow
Merging two people's editsNormal merge, normal conflict markersUsually "pick one"
LayoutAutomatic (you cannot place boxes exactly)Fully yours (and you must move every box when something is added)
Learning curveA small syntax to learnA tool to learn
Pretty, bespoke diagramsLimited: you accept the tool's lookBetter: this is where GUI tools win

The "automatic layout" row is both the cost and the benefit. You give up control of exactly where a box sits and gain the end of tidying. For a design sketch that is the right trade; for a poster, use a drawing tool. Here is the benefit measured on a real change: I committed the library diagram's source and its rendered SVG, then added one class and one relationship, and ran git diff.

diff --git a/docs/diagrams/loans.class.mmd b/docs/diagrams/loans.class.mmd index 6334293..796fe9a 100644 --- a/docs/diagrams/loans.class.mmd +++ b/docs/diagrams/loans.class.mmd @@ -8,9 +8,13 @@ classDiagram borrowedOn dueOn } + class Reservation { + reservedOn + } class Book { title isbn } Member "1" --> "0..*" Loan : borrows Loan "*" --> "1" Book : of + Reservation "*" --> "1" Book : holds a place for
Source diff (the .mmd file): 483 bytes, 4 lines added Rendered SVG diff (same change): 54,559 bytes, one 24 KB line replaced $ git diff --stat docs/diagrams/loans.class.mmd | 4 ++++ docs/diagrams/loans.class.mmd.svg | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-)

The source change is readable at a glance (a new Reservation class and one arrow); the same change in the rendered SVG is a single 24 KB line, and the diff of it is 54,559 bytes of noise. So review the source, not the picture. Whether to commit the rendered files at all is a choice: committing them lets a plain Markdown viewer show them without a build step; not committing them keeps diffs clean and avoids stale pictures. A common compromise is to commit the source and have a build or CI step render the pictures; I have not set up CI here, so I only describe it.

Two Tools, Compared

Mermaid is JavaScript. A diagram is a text block starting with its type (classDiagram, sequenceDiagram), and it is drawn in a browser or by a command-line wrapper that drives a headless browser. PlantUML is Java (here, a native build of it): a diagram sits between @startuml and @enduml and is rendered by a program to SVG, PNG and other formats. Layout for several PlantUML diagram types is done by a separate program, Graphviz.

MermaidPlantUML
NeedsNode and a browser (for the CLI)Java, or the native build used here; Graphviz for some layouts (see below)
Renders natively on GitHubYes: a mermaid fenced block in Markdown (GitHub's documentation, read for this chapter)Not mentioned in that documentation page, so plan on rendering it yourself
Class, sequence, stateYesYes
Use caseYes in 12.x, but the keyword is usecase-beta: treat it as unstableYes
Activity, component, deployment, objectNot as UML diagram types in the syntax I tried (below)Yes, all four (below)
LookModern, themeable, light or dark by a settingPlainer by default; many themes
Error behaviourFails with a parse error, non-zero exitDraws the error into the image (below)

The coverage row was tested, not assumed. I wrote the smallest diagram I could for each type and rendered it with each tool:

--- Mermaid 12.1.0 (mmdc 12.0.0) uc.mmd (first line: usecase-beta): rendered act.mmd (first line: activity): FAILED: UnknownDiagramError: No diagram type detected matching given configuration for text: activity comp.mmd (first line: componentDiagram): FAILED: UnknownDiagramError: No diagram type detected matching given configuration for text: componentDiagram state.mmd (first line: stateDiagram-v2): rendered --- PlantUML 1.2026.8 (native Windows build) uc.puml (use case): rendered act.puml (activity): rendered comp.puml (component): rendered dep.puml (deployment): rendered obj.puml (object): rendered state.puml (state): rendered

Read that carefully. A failure for activity and componentDiagram proves only that those words are not Mermaid diagram types; Mermaid has flowcharts (which can draw an activity-like flow) and an architecture-beta type that I did not test. Mermaid's own syntax reference page (read for this chapter) lists use case but not activity, component, deployment or object diagrams. So the course draws the structure of the data and the interactions in Mermaid, and reaches for PlantUML for the diagram types Mermaid does not offer.

Installing Both (Without Touching Your System)

# Mermaid: Node.js 18+ is needed (24.18.0 here). Install into a project folder, not globally. mkdir ooad && cd ooad npm install @mermaid-js/mermaid-cli@12.0.0 # mermaid-cli drives a browser; point it at one you already have instead of downloading another: # set PUPPETEER_SKIP_DOWNLOAD=1 before npm install, and put the path in a puppeteer config file (below) # PlantUML: download the native build for your OS from # https://github.com/plantuml/plantuml/releases (native-plantuml-windows-amd64-1.2026.8.zip, ~50 MB) # unzip it anywhere; it contains plantuml.exe and the runtime libraries it needs plantuml.exe -version
$ plantuml -version PlantUML version 1.2026.8 / 149874a [2026-09-05 15:59:30 UTC] (GPL source distribution) ... Java Runtime: GraalVM Runtime Environment

The usual way to run PlantUML is java -jar plantuml.jar; I did not use it because Java is not installed on this machine, and the native build needs none (it ships its own runtime, which the Java Runtime: GraalVM line shows). PlantUML also ran here with Graphviz present:

$ plantuml -testdot GraphViz: dot - graphviz version 2.44.1 (20200629.0846) Installation seems OK. File generation OK

Graphviz's dot was already on this machine's path; I do not know where it came from, and I did not test PlantUML without it. If a class or component diagram fails on your machine with a message about dot, install Graphviz (I understand sequence diagrams do not need it, but did not test that). The browser config for mermaid-cli (-p) and a config that sets a plain font (-c) are two small JSON files:

# cfg.json (puppeteer: which browser to use) {"executablePath": "C:/Program Files/Google/Chrome/Application/chrome.exe", "args": ["--no-sandbox"]} # mm.json (mermaid: a system font, so the SVG does not embed a 160 KB web font) {"theme": "default", "fontFamily": "Arial, Helvetica, sans-serif"} mmdc -i diagram.mmd -o diagram.svg -p cfg.json -c mm.json --svgId unique-id

The Same Two Diagrams, in Both Tools

Chapter 1's class and sequence sketches, written first in Mermaid and then in PlantUML. Same meaning, different syntax. Compare how each says "a member has zero or more loans" and "the librarian asks the system".

Class diagram

classDiagram direction LR class Member { name email } class Loan { borrowedOn dueOn } class Book { title isbn } Member "1" --> "0..*" Loan : borrows Loan "*" --> "1" Book : of

borrows

1

0..*

of

*

1

Member

name

email

Loan

borrowedOn

dueOn

Book

title

isbn

Mermaid

@startuml scale 1.5 hide empty methods left to right direction class Member { name email } class Loan { borrowedOn dueOn } class Book { title isbn } Member "1" --> "0..*" Loan : borrows Loan "*" --> "1" Book : of @enduml
MembernameemailLoanborrowedOndueOnBooktitleisbnborrows10..*of*1

PlantUML. hide empty methods hides the empty operations compartment and scale 1.5 enlarges the picture; without it PlantUML's drawing is small on a web page.

Sequence diagram

sequenceDiagram actor M as Member participant L as Librarian participant S as System M->>L: hands over book and card L->>S: borrow(card, isbn) S-->>L: due date, or refusal L-->>M: book and due date
SystemLibrarianSystemLibrarianMemberhands over book and cardborrow(card, isbn)due date, or refusalbook and due dateMember

Mermaid

@startuml scale 1.5 actor Member as M participant Librarian as L participant System as S M -> L : hands over book and card L -> S : borrow(card, isbn) S --> L : due date, or refusal L --> M : book and due date @enduml
MemberLibrarianSystemMemberLibrarianSystemMemberLibrarianSystemhands over book and cardborrow(card, isbn)due date, or refusalbook and due date

PlantUML

IdeaMermaidPlantUML
StartclassDiagram@startuml ... @enduml
Attributes in a classLines inside class X { }Lines inside class X { }
Association with multiplicityA "1" --> "0..*" B : labelThe same text
Call and reply in a sequenceA->>B: text and B-->>A: textA -> B : text and B --> A : text
A personactor M as Memberactor Member as M (the alias comes last)

The two languages are close enough that you can move between them in an afternoon, with one trap in the table: the order of name and alias in actor is reversed. In both tools the arrow --> in a class diagram is an association you can navigate in one direction (chapter 5 covers all the arrow types).

A Render Script

Typing mmdc and plantuml commands for every file is how diagrams go stale. This small script renders every source under a folder, skips the ones whose picture is already newer, and fails loudly (exit status 1) if a diagram has an error. It names each output after its source (x.mmd.svg, x.puml.svg), so a Mermaid and a PlantUML version of one diagram can sit side by side.

"""Render every diagram source under a folder to SVG, next to its source. py render_diagrams.py docs/diagrams *.mmd -> <name>.mmd.svg Mermaid (mmdc, from @mermaid-js/mermaid-cli) *.puml -> <name>.puml.svg PlantUML (plantuml executable or `java -jar plantuml.jar`) The output name keeps the source's extension, so a Mermaid and a PlantUML version of one diagram never overwrite each other. Only sources newer than their SVG are rendered, so it is quick to run before every commit. """ import pathlib import subprocess import sys MMDC = r"C:\lwdj\ooad\node_modules\.bin\mmdc.cmd" # adjust: where you installed mermaid-cli PUML = [r"C:\lwdj\ooad\puml\bin\plantuml.exe"] # or ["java", "-jar", "plantuml.jar"] MMDC_ARGS = ["-p", r"C:\lwdj\ooad\ch1\cfg.json", "-c", r"C:\lwdj\ooad\ch1\mm.json", "-b", "transparent"] # a browser path, and a plain font def stale(src: pathlib.Path, out: pathlib.Path) -> bool: return not out.exists() or out.stat().st_mtime < src.stat().st_mtime def main(folder: str) -> int: failures = 0 for src in sorted(pathlib.Path(folder).rglob("*")): out = src.with_name(src.name + ".svg") if src.suffix == ".mmd": cmd = [MMDC, "-i", str(src), "-o", str(out), "--svgId", "d-" + src.stem.replace(".", "-"), *MMDC_ARGS] elif src.suffix == ".puml": cmd = [*PUML, "-tsvg", "-pipe"] # read the source on stdin, write the SVG on stdout: we choose the file name else: continue if not stale(src, out): print(f"up to date {src}") continue if src.suffix == ".puml": result = subprocess.run(cmd, input=src.read_bytes(), capture_output=True) # PlantUML reports a syntax error by drawing it into the image (and, with -pipe, exits with status 200); a stray exit status 0 is not enough to trust, so check both ok = result.returncode == 0 and result.stdout.lstrip().startswith((b"<?xml", b"<svg")) and b"Syntax Error" not in result.stdout if ok: out.write_bytes(result.stdout) result.stderr = "" if ok else (result.stderr.decode(errors="replace") or "PlantUML drew a syntax error instead of a diagram") else: result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode or not out.exists(): failures += 1 print(f"FAILED {src}\n{(result.stderr or result.stdout).strip()[:300]}") else: print(f"rendered {src} -> {out.name} ({out.stat().st_size // 1024} KB)") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "."))

The real output of running it twice on the folder holding the four diagram sources above, and then on two deliberately broken files:

rendered docs\diagrams\borrow.sequence.mmd -> borrow.sequence.mmd.svg (25 KB) rendered docs\diagrams\borrow.sequence.puml -> borrow.sequence.puml.svg (4 KB) rendered docs\diagrams\loans.class.mmd -> loans.class.mmd.svg (24 KB) rendered docs\diagrams\loans.class.puml -> loans.class.puml.svg (7 KB)
up to date docs\diagrams\borrow.sequence.mmd up to date docs\diagrams\borrow.sequence.puml up to date docs\diagrams\loans.class.mmd up to date docs\diagrams\loans.class.puml
FAILED broken\bad.mmd Error: Parse error on line 2: ...iagram class A { --------------------^ Expecting 'STRUCT_STOP', 'MEMBER', got 'EOF_IN_STRUCT' Parser.parseError (https://mermaid-cli-intercept.invalid/C:/lwdj/ooad/node_modules/mermaid/dist/chunks/mermaid.esm/classDiagram-v2-KCO5GXG6.mjs:353:21) Parser.parse (https: FAILED broken\bad.puml ERROR 1 Syntax Error? (Assumed diagram type: sequence)

Note the last lines: both broken diagrams were reported as failures. The PlantUML one is the interesting one. PlantUML does not refuse to produce a picture for bad input; it draws the error into the picture (here "Syntax Error? (Assumed diagram type: sequence)") and, with -pipe, exits with status 200. The script treats both the exit status and the words "Syntax Error" in the output as a failure. A script that only checked for "a file appeared" would have published the error as if it were your design.

Themes and the Dark Page

UML is conventionally drawn dark on light. This course's pages are dark, so light diagrams sit on a white panel (the .diagram class in the course stylesheet). The alternative is to draw the diagram dark. Mermaid takes a theme in an init comment on the first line; PlantUML has built-in themes, selected with !theme. Both worked offline here:

%%{init: {'theme': 'dark', 'fontFamily': 'Arial, sans-serif'}}%% classDiagram direction LR class Member { name email } class Loan { borrowedOn dueOn } class Book { title isbn } Member "1" --> "0..*" Loan : borrows Loan "*" --> "1" Book : of

borrows

1

0..*

of

*

1

Member

name

email

Loan

borrowedOn

dueOn

Book

title

isbn

Mermaid, theme: dark

@startuml scale 1.5 !theme cyborg hide empty methods left to right direction class Member { name email } class Loan { borrowedOn dueOn } class Book { title isbn } Member "1" --> "0..*" Loan : borrows Loan "*" --> "1" Book : of @enduml
MembernameemailLoanborrowedOndueOnBooktitleisbnborrows10..*of*1

PlantUML, !theme cyborg. The theme names come from PlantUML's bundled list; I tried one, not all of them.

A caution about this convenience: the panel colour must match the diagram. A dark diagram placed on a white panel, or a light one on a dark page without its panel, is the commonest readability bug (chapter 1's class diagram had pale text for exactly that reason). Whichever you choose, look at the diagram on the real page.

Where the Sources Live

Keep the sources next to the code they describe, in the same repository, so a change to the code and a change to the diagram can be one pull request:

library/ ├── src/ │ └── library/ ├── docs/ │ └── diagrams/ │ ├── loans.class.mmd │ ├── borrow.sequence.mmd │ ├── components.puml │ └── README.md # how to render: one command └── render_diagrams.py
  • Name by subject, then type, then tool (loans.class.mmd): you find the diagram by what it is about.
  • One idea per diagram. A diagram that needs scrolling has stopped being a sketch.
  • Mermaid inside Markdown renders on GitHub with no build step, which is the cheapest way to keep a diagram where people already read. Putting a mermaid fence in your README works for the structure and interaction diagrams the course draws in Mermaid. PlantUML needs the render step above.
  • Mind what you send out. PlantUML has a public web server that renders text you send it. A design for a real system may be confidential; render locally, as here.

The Course's Choice

DiagramTool in this courseWhy
Class (chapters 4, 5, 6, 11)MermaidRenders on GitHub; the output is clean; enough notation for what we draw
Sequence (7)MermaidReadable output, simple syntax
State (8)MermaidSupported and clean; chapter 8 will say if PlantUML is needed for composite states or history
Use case (3)PlantUMLMermaid's is beta; PlantUML's is mature
Activity (8)PlantUMLNo Mermaid equivalent (a flowchart is the nearest)
Component, deployment, object (9)PlantUMLNot Mermaid UML types

That is a decision with a cost: you learn two syntaxes. I judge it worth it, because the alternative is to leave out half the diagram types or draw them badly. If you prefer to use one tool, PlantUML alone covers everything in this course; Mermaid alone does not.

Mistakes while preparing this chapter
  • My course outline was out of date. It said the latest PlantUML was V1.2026.6 and that PlantUML needs Java, "not installed on this machine". Checking the releases page showed 1.2026.8 (2026-09-05), with a native Windows build that needs no Java. What went wrong: I carried a version and a prerequisite from notes written earlier. How to avoid it: re-check versions and requirements on the day you use them.
  • My render script overwrote its own output. The first version named every picture after its source's stem, so loans.class.mmd and loans.class.puml both wrote loans.class.svg; the second silently replaced the first, and the "up to date" check then believed the wrong file. Fixed by keeping the source extension in the output name. Takeaway: when two inputs can map to one output name, the tool will lose one without telling you; test with the awkward case on purpose.
  • I tested error handling with input that was not an error. My first "broken" PlantUML file was A -> with no target, which PlantUML accepts as a sequence message, so the script correctly reported success. I then assumed PlantUML exits 0 on syntax errors; with a really broken file it exits 200 and draws the error. Takeaway: before you write a check, produce a failure you have verified is a failure, and read the exit status instead of guessing it.
  • A failing probe proves less than it seems. Mermaid rejected my guessed keywords activity and componentDiagram. That shows those words do not exist, not that Mermaid cannot draw such things. I limited the claim in the comparison to what I had tested and what Mermaid's syntax reference lists.

Hands-On Exercises

Exercise 1

In Mermaid, draw the lifecycle of a library loan as a state diagram: it starts when the book is borrowed, can become overdue, and ends when it is returned (an overdue loan returns with a fine due). Render it with the script.

📄 View solution
Exercise 2

In PlantUML, draw a use case diagram for the library: a Member can borrow, return and pay a fine; a Librarian can borrow and return on a member's behalf. Draw the system boundary. Render it with the script.

📄 View solution
Exercise 3

Set up a diagrams folder with one Mermaid and one PlantUML source, and use the render script to show four things: the first render, a no-change run that skips both, a run after editing one file that re-renders only that one, and a run with a broken file that fails with exit status 1.

📄 View solution

Chapter 2 Quick Reference

  • Diagrams as text: reviewable diffs, history, merges, regenerated by command; review the source, not the picture (the SVG diff was 54,559 bytes against a 483-byte source diff)
  • Mermaid 12.1.0 + mermaid-cli 12.0.0: mmdc -i x.mmd -o x.svg -p cfg.json -c mm.json --svgId unique; renders natively in GitHub Markdown; class, sequence, state; use case only as usecase-beta
  • PlantUML 1.2026.8: @startuml ... @enduml; plantuml -tsvg -pipe; native build needs no Java; Graphviz for some layouts (untested without); covers use case, activity, component, deployment, object
  • PlantUML draws syntax errors into the picture (exit status 200 with -pipe): check for it in scripts
  • Keep sources beside the code, named subject.type.tool; one idea per diagram; render locally for confidential designs
  • Dark page: light diagrams on a white panel, or draw dark (theme: dark, !theme cyborg); look at the real page
  • Course choice: Mermaid for class, sequence, state; PlantUML for use case, activity, component, deployment, object
Coming next
Object-Oriented Design with UML 3 (From Requirements to Use Cases): actors, goals and scope, the use case diagram and its limits, writing a use case description with a main flow and alternative flows, and finding the use cases of the library system.