PlantUML: Powerful Diagrams with Code

At some point in most software projects, someone opens a Visio or Lucidchart file, updates three boxes, screenshots it, uploads the image to Confluence, and calls it “documentation.” Six months later that diagram is wrong, the person who knew how to update it has left, and nobody can remember what the arrows mean. I have lived this. PlantUML is the answer I landed on, and it’s been part of my workflow ever since.
The core idea is simple: you write a text description of your diagram, and PlantUML generates the image. Because it’s text, it lives in your repository, diffs cleanly in pull requests, and can be regenerated in CI. No proprietary file formats. No “you need to install this app.” No screenshot-and-pray documentation workflows.
Key Takeaways
- PlantUML diagrams are plain text — they belong in version control, not in a shared drive folder nobody remembers
- Supports sequence, class, component, state, activity, use case, and entity-relationship diagrams out of the box
- Integrates directly with VS Code, IntelliJ, and most CI/CD pipelines — low friction to add to an existing workflow
- Text-based means diagrams can stay in sync with code; whether they do stay in sync is still up to you
- It’s not the prettiest output by default, but skinparam and themes give you enough control to make it presentable
🧩 What is PlantUML?
PlantUML is a text-based diagramming tool created by Arnaud Roques in 2009. You write a description of your diagram using a purpose-built syntax, and PlantUML converts it to an image. That’s it. The genius is in what that enables downstream: version control, code review, CI automation, and a documentation workflow that doesn’t depend on anyone having the right desktop app installed.
It supports a wide range of diagram types — more on that below — and has plugins for VS Code, IntelliJ, Eclipse, and several others.
⚙️ How PlantUML Works
You write a text file, PlantUML generates the image. Here’s a minimal sequence diagram:
@startuml
Alice -> Bob: Hello Bob, how are you?
Bob --> Alice: I am good thanks!
@enduml

That’s seven lines of text to produce a readable sequence diagram. No clicking, no dragging arrows around on a canvas, no wondering if your exported image is current.
Supported Diagram Types
PlantUML covers most of what you’ll need day to day:
- Sequence Diagrams: Interactions between components over time — great for API flows and auth sequences
- Class Diagrams: Static structure, relationships, inheritance
- Component Diagrams: System topology, dependencies, service boundaries
- State Diagrams: State machines and object lifecycle
- Activity Diagrams: Workflows and process flows
- Entity-Relationship Diagrams: Database schema — useful when you’re designing or debugging a data model
🤔 Why PlantUML (Over the Alternatives)?
Let me actually answer this rather than just list features.
It’s text. That means diffs. That means code review. That means you can see when a diagram was last touched and who touched it. None of the GUI tools give you this — they give you a binary blob that either isn’t in version control at all or commits as “diagram.vsdx changed” with no useful diff.
It runs in CI. You can regenerate every diagram in your docs as part of a build. If something breaks, the build breaks. This is how documentation rot gets caught before it spreads.
It’s not vendor-locked. PlantUML is open source, runs on Java, and can be self-hosted. The syntax files are just text. If PlantUML ever disappears, your diagrams don’t — you still have the source and can render them with any compatible tool.
The tradeoff is real though: it produces functional diagrams, not beautiful ones by default. If you need polished, presentation-quality visuals for a pitch deck or an executive briefing, there are better tools. PlantUML is a documentation tool, not a design tool. Know the difference before you commit to it.
⚖️ A Quick Comparison with Other Tools
| Tool | Good for | Watch out for | In version control? |
|---|---|---|---|
| PlantUML | Developer docs, CI-generated diagrams, anything that needs to evolve with code | Not pretty by default; learning curve on the syntax | ✅ Native — it’s just text |
| Mermaid | Same use cases as PlantUML, native GitHub/GitLab rendering | Fewer diagram types; complex diagrams get unwieldy fast | ✅ Native — also just text |
| Draw.io | Quick one-offs, whiteboard-style collaboration | Manual export cycle; XML diffs are not human-readable | ⚠️ Technically yes, practically no |
| Lucidchart | Stakeholder-facing diagrams, polished outputs | SaaS-only, subscription cost, no meaningful versioning | ❌ |
| Microsoft Visio | Enterprise environments where everyone has it licensed | Expensive, old-feeling, collaboration is painful | ❌ |
Aside: Mermaid is worth calling out specifically because it renders natively inside GitHub and GitLab markdown. If your audience is primarily reading diagrams on a git host rather than in built documentation, Mermaid has a real convenience edge. PlantUML requires a running server or a build step to produce images. Both are valid — the right answer depends on where your diagrams actually get read.
🛠️ Practical Tips
A few things I’ve learned using PlantUML on real projects:
Keep individual diagrams focused. A sequence diagram that tries to show every variant of a flow is unreadable. Draw the happy path first. Then draw the error case separately. Two clear diagrams are better than one that requires a legend.
Use !include to share common definitions. If you have a set of actors, participants, or skinparam settings used across multiple diagrams, pull them into a shared .iuml file and include it. This is especially useful when you’re generating docs from a repository — one skin change updates everything.
Use annotations and notes. PlantUML supports note left of, note right of, and inline block notes. Use them for the decisions you want to preserve — the why behind a sequence, not just the what. Diagrams without notes tend to lose their meaning faster than you’d expect.
Group and package related elements. The package, node, and rectangle groupings do a lot to make component diagrams legible at a glance. Here’s a simple example:
@startuml
package "Frontend" {
[Login Page] --> [Dashboard]
[Dashboard] --> [Profile Page]
}
package "Backend" {
[API Gateway] --> [Authentication Service]
[API Gateway] --> [Data Service]
}
[Login Page] -> [API Gateway]
@enduml

This diagram clearly separates frontend and backend components. Anyone reading this immediately knows where the API boundary is, without needing to read the surrounding text.
📋 Examples
Here are examples of the main diagram types, with context for when you’d actually reach for each one.
Sequence Diagram
Use for: API flows, auth sequences, anything where the order of messages between systems matters.
@startuml
autonumber
actor User as user
participant "Browser UI" as browser
participant "Reseller UI" as reseller_ui
user -> browser : Visit the Reseller UI login page
browser -> reseller_ui : Retrieve the Reseller UI login page
browser <- reseller_ui : Return the login page with form field \nusername, password, and One Time Password(OTP)
user <- browser : Display the page, wait for user input
user -> user: Recall username and password \nfrom memory
user -> browser : Fill in the username and password field
user -> user: Open Google Authenticator, \nread the OTP
user -> browser : Fill in the OTP, and hit the send button
browser -> reseller_ui : Send the username, password and OTP
reseller_ui -> reseller_ui : Verify the information is valid
alt Login valid
browser <- reseller_ui : Return the logged in page
user <- browser : Display the logged in page
else Login invalid
browser <- reseller_ui : Return login failure page
user <- browser : Display the login failure page
end
@enduml
Class Diagram
Use for: modeling object relationships, documenting inheritance hierarchies, or producing a quick reference for an existing codebase.
@startuml
hide circles
Magic : numberActiveSpells : integer
Magic : totalManaCostPerTurn : integer
Magic <|-- Spells
Spells : spellNumber[]: integer
Spells : powerLevel: integer
Spells : mannaPerTurn: bool
Spells : mannaPerTarget: bool
Spells : castSpell()
Magic <|-- CounterSpells
CounterSpells : spellNumber[]: integer
CounterSpells : powerLevel: integer
CounterSpells : castCounterSpell()
Magic <|-- MagicSupport
MagicSupport : mannaExpending: integer
MagicSupport : powerLevel: integer
MagicSupport : expendManna()
MagicSupport : getPowerLevel()
@enduml
Use Case Diagram
Use for: capturing what actors can do in a system — particularly useful early in requirements gathering when you need to communicate scope without getting into implementation details.
@startuml
skinparam packageStyle rect
(Start) <|-- :Player_1:
Player_1 <|-right- :Player_2:
(GamePlay) <-right-(Start)
(Movement) <-- (GamePlay)
(Move) <-- (Movement)
Note top of (Move): Player moves in given direction
(Combat) <-- (GamePlay)
Rectangle {
(Attack) <-- Combat
(Defend) <-- Combat
}
usecase (Access Menus) as Menus
Menus <-- (GamePlay)
(Upgrades) <-- Menus
Rectangle {
(Purchase) <-- Upgrades
(Utilize) <-- Upgrades
}
(Inventory) <-- Menus
Rectangle {
(Use Item) <-- Inventory
(Drop Item) <-- Inventory
(Info) <-- Inventory
}
Note right of Info: Short description of item
(Purchasing) <-- Menus
Rectangle {
(Spend) <-- (Purchasing)
(Receive) <-- (Purchasing)
}
(Purchasing) <-- Purchase
@enduml
Activity Diagram
Use for: process flows and decision logic. I reach for these when I need to document a workflow with branching paths — onboarding sequences, approval processes, retry logic.
@startuml
start
:Open and Read Layout Configuration XML;
repeat
:**Read Process**;
repeat
:**Read Thread**;
note: at least one should be there, the main thread
repeat
:**Read Component**;
repeat
: **Read Interface**;
repeat while(interfaces?)
: **Generate Interfaces Code**;
repeat while(components?)
: **Generate Components Code**;
repeat while(threads?)
: **Generate Threads Code**;
repeat while(processes?)
:**Generate Processes Code**;
stop
@enduml
Component Diagram
Use for: high-level system topology — which services exist, how they connect, where the data flows. Good for onboarding new engineers or explaining a system to someone who doesn’t need to see the code.
@startuml
skinparam backgroundcolor transparent
node Backend
database Database
node Frontend
component Collectd
agent Client1
agent Client2
agent Client3
agent Client4
Frontend -- Backend
Backend -- Database
Backend -- Collectd
Collectd .. Client1
Collectd .. Client2
Collectd .. Client3
Collectd .. Client4
@enduml

State Diagram
Use for: modeling object or entity lifecycle — order status, user account states, connection states. Anywhere you have a defined set of states and specific triggers that move between them.
@startuml
title performing I/O
[*] --> Client
Client: Process uri in Client
Client -> Server : uri-data
state Server {
[*] -> monitor
monitor: Server starts monitoring
monitor: Stops at end of Server life
}
Client -> Database: Client
Database: database operations
state runcommand {
Database -> command: Database
command -> find: query
command: send command to server
monitor -> select: server-data
find --> select
select: select process to find proper server
select --> find: Server
find --> query: server
query: encodes query and send to server
query: decodes result
query --> find: result
}
find -> Cursor: cursor-data
Cursor: stores docs
Cursor: retrieves new docs
Cursor -> fetch: document
fetch --> getmore
getmore: encodes query and send to server
getmore: decodes result
getmore --> fetch: new-documents
fetch -> [*]
@enduml
🏁 Wrapping Up
PlantUML won’t make your documentation problems disappear — but it will make them much harder to ignore. When diagrams live in the same repository as the code they describe, the friction of keeping them current drops enough that people actually do it. That’s the real win here.
If this saved you some research time, pass it on.