PlantUML ER diagrams: data modeling & table relationships

puml.online

ER diagrams (entity-relationship) are usually the first step in database design. PlantUML’s ER syntax is weaker than Mermaid’s, but it works and stays in the same family as your other UML diagrams.

One-line definition

An ER diagram shows how tables (entities) relate: 1:1, 1:N, N:M, weak entities, derived attributes. It’s the bridge between the domain model and the physical schema.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@startuml
hide circle

entity User {
*id : BIGINT <<PK>>
--
*email : VARCHAR(255) <<UK>>
name : VARCHAR(100)
status : ENUM('active', 'banned', 'pending')
created_at : TIMESTAMP
}

entity Order {
*id : BIGINT <<PK>>
--
*user_id : BIGINT <<FK>>
*status : ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled')
total : DECIMAL(10,2)
created_at : TIMESTAMP
}

entity OrderItem {
*id : BIGINT <<PK>>
--
*order_id : BIGINT <<FK>>
*product_id : BIGINT <<FK>>
quantity : INT
unit_price : DECIMAL(10,2)
}

entity Product {
*id : BIGINT <<PK>>
--
*sku : VARCHAR(50) <<UK>>
name : VARCHAR(255)
price : DECIMAL(10,2)
stock : INT
}

User ||--o{ Order : places
Order ||--|{ OrderItem : contains
Product ||--o{ OrderItem : appears in

note top of User
ER notation:
|| exactly one
|o zero or one
}o many
{ required (one or more)
end note
@enduml

Three core relationship styles

1
2
3
4
5
A ||--|| B        : A to B, one to one (exactly)
A ||--o{ B : A to B, one to many (B end has 0..n)
A }o--o{ B : A to B, many to many
A ||--|{ B : A to B, one to many (B end required)
A }|--|{ B : A to B, many to many (both required)
Left Right Meaning
` `
` `
` `
}o }o many to many

Derived relations

Self reference

1
2
3
4
5
6
7
8
9
10
11
12
13
@startuml
hide circle

entity Employee {
*id : BIGINT <<PK>>
--
name : VARCHAR
*manager_id : BIGINT <<FK>>
}

Employee ||--o{ Employee : manages

@enduml

The Employee table’s manager_id references its own id — represents a reporting line.

Derived attribute

PlantUML doesn’t have a built-in “derived” marker. Use <<computed>> or a note:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@startuml
entity Order {
*id : BIGINT <<PK>>
--
*user_id : BIGINT <<FK>>
subtotal : DECIMAL(10,2)
tax : DECIMAL(10,2)
--
total : DECIMAL(10,2) <<computed>>
}

note right of Order
total = subtotal + tax
maintained by trigger or application layer
end note
@enduml

Many-to-many via join table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@startuml
hide circle

entity Student {
*id : BIGINT <<PK>>
name : VARCHAR
}

entity Course {
*id : BIGINT <<PK>>
title : VARCHAR
}

entity Enrollment {
*id : BIGINT <<PK>>
--
*student_id : BIGINT <<FK>>
*course_id : BIGINT <<FK>>
grade : DECIMAL(5,2)
enrolled_at : TIMESTAMP
}

Student ||--o{ Enrollment : enrolls
Course ||--o{ Enrollment : has
@enduml

A join table represents many-to-many, often with its own fields.

Real example: blog system

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@startuml
hide circle

entity User {
*id : BIGINT <<PK>>
--
*email : VARCHAR(255) <<UK>>
name : VARCHAR(100)
bio : TEXT
role : ENUM('admin','editor','member')
created_at : TIMESTAMP
updated_at : TIMESTAMP
}

entity Post {
*id : BIGINT <<PK>>
--
*author_id : BIGINT <<FK>>
*slug : VARCHAR(255) <<UK>>
*title : VARCHAR(500)
*body : TEXT
*status : ENUM('draft','published','archived')
published_at : TIMESTAMP
created_at : TIMESTAMP
updated_at : TIMESTAMP
}

entity Tag {
*id : BIGINT <<PK>>
--
*name : VARCHAR(50) <<UK>>
description : VARCHAR(255)
}

entity PostTag {
*post_id : BIGINT <<FK>>
*tag_id : BIGINT <<FK>>
--
}

entity Comment {
*id : BIGINT <<PK>>
--
*post_id : BIGINT <<FK>>
*author_id : BIGINT <<FK>>
*body : TEXT
created_at : TIMESTAMP
}

User ||--o{ Post : writes
User ||--o{ Comment : leaves
Post ||--o{ Comment : owns
Post ||--o{ PostTag : tagged with
Tag ||--o{ PostTag : tags
@enduml

Review checklist

  • PK marked with <<PK>>?
  • FK marked with <<FK>>?
  • Unique keys marked with <<UK>>?
  • Cardinality right? ||--o{ vs ||--|{
  • Table / column names follow conventions?
  • Many-to-many has a join table?
  • Self-reference explained?

Tool comparison

Tool Strength Weakness
PlantUML ER Text-first, diff-friendly, same family as UML Layout less polished
Mermaid erDiagram Renders natively on GitHub Limited syntax
draw.io WYSIWYG, free-form layout Not text-friendly
dbdiagram.io Dedicated ER DSL PNG/SVG, no merge
MySQL Workbench / DataGrip Complete DDL bidirectional Heavy; needs Java / desktop

From ER to DDL

PlantUML doesn’t generate DDL directly. Use the ER diagram as the design source, then run migrations via Flyway / Liquibase:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# .github/workflows/db-from-er.yml
name: Generate DDL from ER
on:
paths: ['docs/erd/**']

jobs:
ddl:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Render ER diagrams
run: |
# auto-render .puml ER files to .svg for review
...
- name: Hand-write / review migrations
if: github.event_name == 'pull_request'
run: |
# CI checks that docs/erd/*.puml is consistent with migrations/*.sql
...

Note: ER diagram → DDL is not automatic. Indexes, constraints, defaults, character sets need DBA or migration tooling review.

  • Title: PlantUML ER diagrams: data modeling & table relationships
  • Author: puml.online
  • Created at : 2026-07-29 15:35:00
  • Updated at : 2026-08-14 21:34:29
  • Link: https://puml.online/blog/plantuml-er-en/
  • License: This work is licensed under CC BY-NC-SA 4.0.