All posts
MermaidClass DiagramsSoftware Architecture

Domain modeling with Mermaid class diagrams

5 min readThe MermaidCreator team

Class diagrams have a reputation problem. In many teams they're associated with heavyweight UML tooling, enterprise process, and documentation that's out of date before it's published. That reputation is deserved — but it's a tooling problem, not a concept problem.

The concept is genuinely useful: a shared picture of your domain model accelerates design discussions, surfaces missing relationships, and gives new engineers a map of the codebase before they open a single file. Mermaid's classDiagram syntax keeps the concept without the baggage. The diagram is text, it lives in your repository, and it gets reviewed alongside the code it describes.

Class diagram basics

A class has three compartments: name, attributes, and methods. In Mermaid, you declare them one per line inside curly braces:

classDiagram
    class Order {
        +String id
        +String status
        +Decimal total
        +DateTime createdAt
        +place() void
        +cancel() bool
        +calculateTotal() Decimal
    }

Visibility prefixes follow UML convention:

PrefixVisibility
+Public
-Private
#Protected
~Package / internal

Relationships

The real value of a class diagram is in the lines between classes. Mermaid supports all the standard UML relationship types:

SyntaxRelationshipWhen to use
-->AssociationOne class uses another
*--CompositionChild cannot exist without parent
o--AggregationChild can exist independently
<|--InheritanceIs-a relationship
..|>ImplementationImplements an interface
..>DependencyUses temporarily

Multiplicity labels go in quotes beside the class name on each end of the arrow:

classDiagram
    Customer "1" --> "0..*" Order : places
    Order "1" *-- "1..*" LineItem : contains
    LineItem "0..*" --> "1" Product : references
    Order "1" --> "0..1" Payment : paid by

Skip multiplicity when the cardinality is obvious; add it when it's not.

A practical example: e-commerce domain

Here's a domain model for a small e-commerce system — the kind of diagram you'd sketch in the first week of a project:

classDiagram
    class Customer {
        +String id
        +String email
        +String name
        +register() void
    }

    class Order {
        +String id
        +OrderStatus status
        +Decimal total
        +place() void
        +cancel() bool
        +calculateTotal() Decimal
    }

    class LineItem {
        +String productId
        +int quantity
        +Decimal unitPrice
        +subtotal() Decimal
    }

    class Product {
        +String id
        +String name
        +Decimal price
        +int stockLevel
        +isAvailable() bool
    }

    class Payment {
        +String id
        +PaymentMethod method
        +Decimal amount
        +process() bool
    }

    Customer "1" --> "0..*" Order : places
    Order "1" *-- "1..*" LineItem : contains
    LineItem "0..*" --> "1" Product : references
    Order "1" --> "0..1" Payment : paid by

This diagram won't match the code exactly — the code has more fields, more methods, and more edge cases. That's intentional. A domain diagram is a map, not a mirror.

Interfaces and inheritance

Use <<interface>> to mark abstract types and ..|> to show implementation:

classDiagram
    class PaymentProcessor {
        <<interface>>
        +charge(amount: Decimal) bool
        +refund(transactionId: String) bool
    }

    class StripeProcessor {
        -String apiKey
        +charge(amount: Decimal) bool
        +refund(transactionId: String) bool
    }

    class PayPalProcessor {
        -String clientId
        +charge(amount: Decimal) bool
        +refund(transactionId: String) bool
    }

    PaymentProcessor <|.. StripeProcessor : implements
    PaymentProcessor <|.. PayPalProcessor : implements

The dotted line on <|.. signals implementation rather than inheritance — the distinction matters when you're communicating with engineers coming from statically typed languages where the two are explicit in the syntax.

Enumerations

Enums are first-class in Mermaid class diagrams and make valid state values explicit:

classDiagram
    class OrderStatus {
        <<enumeration>>
        PENDING
        CONFIRMED
        SHIPPED
        DELIVERED
        CANCELLED
    }

    class Order {
        +String id
        +OrderStatus status
    }

    Order --> OrderStatus : has status

Naming the enum in the diagram prevents the inevitable "wait, what values can status actually hold?" question in code review.

Generics

Use tilde syntax for generic types — useful when documenting collection-heavy APIs:

classDiagram
    class Repository~T~ {
        +findById(id: String) T
        +findAll() List~T~
        +save(entity: T) T
        +delete(id: String) void
    }

    class OrderRepository {
    }

    Repository~Order~ <|-- OrderRepository : extends

Tips for useful class diagrams

Model the domain, not the database. A class diagram should reflect the business concepts your code works with, not the table structure underneath. Foreign keys belong in an ER diagram; relationships between domain objects belong here.

Don't model every class. Pick the ten or fifteen classes that carry the most business logic. Leave out DTOs, value objects, and persistence entities unless they're the point of the diagram.

Draw before you code. Class diagrams find missing concepts. Trying to draw the relationship between Order and Discount and realizing you haven't worked out stacking rules is a five-minute design session, not a two-day refactor.

Keep it in the same PR as the code change. A class diagram in docs/architecture/domain.md earns its keep only if it's updated when the model changes. The habit of reviewing diagram alongside code is what keeps it accurate.

Class diagrams vs. ER diagrams

Both show relationships between named things, which makes them easy to confuse. The key distinction: an ER diagram models your storage layer — tables, columns, foreign keys, cardinalities as the database enforces them. A class diagram models your object layer — classes, methods, inheritance, and the associations your code actually traverses.

In a well-layered system, they won't look the same. An ER diagram may have a join table your domain model abstracts away. A class diagram may have a polymorphic type your schema represents with nullable columns. Both are right; they're describing different things.

If you can't describe what a relationship arrow means in plain English, it doesn't belong on the diagram.

Start with the five or six most important classes in your system. Draw the relationships between them. The gaps you find in the first ten minutes are worth more than the finished diagram.

Related posts