A class diagram creator, for the way I keep design notes, is a text file you can diff. You name the types, mark the members a reviewer should actually argue about, and draw the lines that mean inheritance, composition, or a plain association. The preview is the picture. The source is what you commit. If the job is a UML class diagram next to a billing change, you do not need a drawing program. You need a small model and the nerve to stop before every getter lands in a box.
The file is the creator
I keep diagrams in git because the argument is usually about a line, not a pixel. A teammate can comment on Account <|-- TeamAccount the same way they comment on an extends clause. They can see that you flipped the arrow. They cannot do that with a screenshot of boxes someone dragged around last quarter and exported as a PNG nobody wants to reopen.
The keyword is classDiagram. A class is a name and an optional body. The body holds fields and methods. Relationships sit outside the bodies, one per line, so a review can quote a single line. That split matters more than the theme. If you bury the inheritance inside a paragraph under the figure, the next edit will update the picture and leave the paragraph stale, or the other way around. Put the claim in the line.
A generated first draft is fine when you have a noun list and no file yet. The AI class diagram generator will turn "accounts, invoices, lines, payments" into boxes. I still read every arrow before I commit. Generated inheritance points the wrong way often enough that I treat the first draft as a sketch, not a model. The class diagram syntax page is where the markers are defined in one place. This post is about using them on a billing model without lying.
Live preview is the other half of the work. You type a line, the boxes move, and you notice that Line ended up as a sibling of Invoice because the association was backwards. That feedback is why I want an editor I can open without signing up. If you would rather rename a larger domain than invent one, start from the template library and delete boxes until the picture matches the change you are actually shipping.
Visibility marks, and when to skip them
+ is public, - is private, # is protected. Mermaid also accepts ~ for package visibility. I use the marks when the diagram is about an API boundary. I skip them when the diagram is about the domain and the reader will stop to ask what the plus sign means. A product review that stalls on notation has already failed.
A field reads as a type and a name: +int totalCents. A method keeps parentheses: +send(). I don't write a return type on every method unless the return is the thing under review. +send() says the invoice can be sent. +balance() int says the type of the answer matters. Pick one style per diagram and stick to it. Mixing +int total and a trailing return type in the same file will render, and it will also start a style argument that has nothing to do with billing.
Private fields are the mark I see abused. People put -string passwordHash on a user box in a domain diagram because the code has that field. The diagram then looks like a class listing copied out of an IDE. If the review is "does TeamAccount inherit from Account," the hash is noise. If the review is "which type is allowed to see the hash," the mark is the whole point, and the rest of the model should get out of the way. Those are two different pictures. Don't make one diagram do both.
Protected is the mark I almost never need on a billing sketch. It means subclasses can see the member and unrelated code cannot, which is a language rule. If the language you ship doesn't have protected, don't decorate the diagram with a symbol your readers will misread as "sort of private." Leave it off and mention the access rule in the pull request, where a sentence is allowed to be a sentence.
Static members get a $ suffix in Mermaid, and abstract methods get a *. I use those only when the review is about the language feature. A billing domain diagram with +count$ on Account usually means someone was showing off the syntax. The seat count is a field. It is not more architectural because you marked it static.
Inheritance is not composition
These two lines get swapped by people who learned both arrows from one slide and then didn't have to maintain the picture.
Account <|-- TeamAccount means TeamAccount is a kind of Account. The hollow triangle sits on the parent. I say that out loud while I type it, because the parser will accept the reverse and your reviewer might not. TeamAccount <|-- Account is valid syntax and a false model. It says a plain account is a special team account. Nothing in the preview turns red. The picture just quietly teaches the wrong hierarchy to the next person who pastes it into a design doc.
Invoice *-- Line is composition. The line item does not outlive the invoice. Delete the invoice in the domain story and the lines go with it. Account o-- Invoice is aggregation. The account groups invoices. An invoice can still be discussed after the login is closed, which is what finance actually does during an audit. If your product really destroys invoice rows when the account row goes, use composition and be ready to defend it. I have lost that argument to an accountant, and the accountant was right. Legal records are a bad place to show off a filled diamond.
A numbered association is the third kind of line, and it is the one that bites people who copy a snippet and drop the quotes. Invoice "1" --> "*" Payment : settles says one invoice settles many payments. Both numbers are quoted. A bare * is the composition token, so Invoice "1" --> * Payment fails to parse and the rest of the diagram may not draw. The colon label is optional for the parser and required for your reader. "settles" is the verb. Without it, cardinality is a puzzle the reader has to solve by guessing which box owns the foreign key.
Don't draw inheritance and composition between the same pair. A TeamAccount is not composed of Account. It is an Account. If you feel the need for both arrows, you have two relationships and you should name them, or you have one relationship and you haven't decided which lifetime you mean. The diagram can't store your uncertainty. It will just show both and look authoritative.
A small billing model
Here is the model I want in a README when a billing change is up for review. A team account is a kind of account. Accounts group invoices. An invoice is composed of lines. An invoice settles one or more payments. That is the whole claim. If a box doesn't serve that claim, it doesn't go in this file.
classDiagram
class Account {
+string id
+string name
+close()
}
class TeamAccount {
+string orgName
+int seatCount
}
class Invoice {
+string id
+int totalCents
+send()
}
class Line {
+string sku
+int amountCents
}
class Payment {
+string id
+int amountCents
}
Account <|-- TeamAccount
Account o-- Invoice : groups
Invoice *-- Line : contains
Invoice "1" --> "*" Payment : settlesTeamAccount could have been an empty class. Mermaid will draw a box with only a name, which is handy while you are still deciding the subtype exists. I gave it two fields so a reviewer can see why the subtype exists: an org name and a seat count that a personal account does not have. If those fields later move up to Account, the inheritance is probably wrong. Delete the subtype instead of keeping an empty box so the diagram still looks like UML.
totalCents is an int on purpose. I don't put a float called money on a class diagram I'm willing to sign. If someone wants a Money type, that is a new box with a currency field, not a prettier name on the int. The diagram is a good place to force that conversation because the field is short and the argument is visible in the diff. A renamed field is one line. A new class is a new relationship, and the review changes shape, which is what you want.
close() lives on Account, not on TeamAccount. The subtype inherits it. If team closure has extra rules, add +close() on TeamAccount and write one sentence under the figure about the override. Don't add a second inheritance arrow to "show" the override. There is one parent. The method line is the override.
The composition line has no cardinality text. *-- already says the invoice owns the parts. Adding a quoted star on that same line is how you get two stories for one pair. The payment line has quotes and a verb because "many payments" is the fact a finance reviewer came to check. One invoice, many payments, and the invoice is the one side. A payment does not settle many invoices in this model. If your product does split a payment across invoices, this line is wrong and you should change it before the meeting, not during the meeting.
The port, drawn separately
The first diagram is the domain. The second is the boundary I add when someone asks how an invoice actually charges a card. I don't paste the gateway into the first picture. It changes the question from "what do we store and subtype" to "what do we call." Those questions have different reviewers. The domain review wants a product person and someone who has read the schema. The gateway review wants the person who owns the card integration. One poster makes both of them skim.
classDiagram
class PaymentGateway {
<<interface>>
+charge()
}
class CardGateway {
+charge()
}
class Invoice {
+int totalCents
+send()
}
PaymentGateway <|.. CardGateway
Invoice ..> PaymentGateway : uses<<interface>> is a stereotype inside the class body, not a visibility mark. CardGateway realizes PaymentGateway. The dotted triangle PaymentGateway <|.. CardGateway points at the interface, the same parent-side rule as inheritance. Invoice ..> PaymentGateway is a dependency: the invoice uses a gateway and does not own one. If you write Invoice *-- PaymentGateway, you have said the gateway's lifetime is the invoice's lifetime, which is a very strange payment provider. The provider outlives every invoice you will ever send.
I leave +charge() without arguments until the review is about arguments. The day someone asks whether charge takes cents or a Money object, I add the argument in the same commit as the code. Before that, the parentheses are enough to show it is a method and not a field. Empty parentheses are a claim that the signature is not under discussion. Don't fill them with placeholders like +charge(x) just to look complete. A fake argument becomes a real one in somebody's head.
This split is an opinion I'll keep. One diagram for the billing nouns. One diagram for the port you call. A single poster with Account, Invoice, Line, Payment, Gateway, Card, Webhook, and a retry policy is how class diagrams become wallpaper. Nobody reviews wallpaper. They skim it, approve the pull request, and discover the lifetime bug three weeks later when a refund can't find its invoice.
Mistakes that still render
The parser catches a missing quote. It does not catch a false model. These are the failures I actually see in reviews.
- Pointing
<|--at the child.Account <|-- TeamAccountputs the hollow triangle on Account. Swap the names and the subtype looks like the base class. The preview will not save you. Read the triangle, not the left-to-right order of the words. - Writing
Invoice "1" --> * Paymentwithout quotes around the star. The bare star is read as composition and the line fails. Use"1" --> "*". Quote both sides even when the one side feels obvious. - Using
*--because the boxes look nicer with a filled diamond. Composition is a lifetime claim. If the child row outlives the parent in the product, you wanted aggregation or a numbered association. Pretty arrows are not a model. - Stuffing getters, setters, and constructors into every box. The diagram becomes a class listing. Keep the members a reviewer could reject. If you wouldn't comment on the method in review, it doesn't earn a line.
- Drawing database keys on the class boxes. Primary keys and foreign keys belong on an ER diagram. A class diagram that grows a fake
customerId FKfield is drifting into a schema, and it will disagree with the migration the same week.
The fifth one is the mistake I make when I'm tired. The class file starts to "help" by showing ids that match the tables. Then someone updates a column and updates only one picture. If you need both pictures, link them and let them disagree in the open until you fix the model. An ERD diagram creator is the right next read when the boxes are tables. Don't make the class diagram do that job badly and then wonder why the two files drift.
A related failure is modeling a status enum as a subclass. Invoice <|-- PaidInvoice looks clever and then you can't show an invoice that moves from draft to paid without destroying the object. Status is a field, or it is a state diagram. It is not an inheritance tree unless the subtypes have different fields and different methods that you are willing to name.
Where the class picture stops
A class diagram answers "what exists, and how the types hold each other." It does not answer "what happens at checkout." That conversation is a different file, with participants and a dotted reply. It does not answer "which rows are allowed to be missing." That is cardinality on an ER diagram, and a crow's foot will not appear here no matter how you decorate a method. If you find yourself adding notes to explain a sequence of calls, you have the wrong picture open.
If the fence itself is still unfamiliar, what a Mermaid diagram is is the short orientation, and how to make a diagram walks from an empty file to a rendered picture. I would not start there if you already have the billing nouns in your head. Start with the two diagrams above. When you want inheritance used on a different domain, the UML class diagram example is a blog-shaped model with a repository interface, which is a useful contrast to this billing slice. Mermaid diagram examples are the right hop once you want to see other diagram types beside this one, so you don't reach for class boxes when you meant a flowchart of the refund policy.
I keep the file at docs/billing-classes.md, or next to the package it describes. The path matters less than the habit. The diagram changes in the same pull request as the types. A class diagram that updates a week later is a souvenir of a decision you already shipped. Reviewers can feel that. They stop trusting the picture, and then you are maintaining text nobody reads, which is worse than having no diagram.
Open the editor, paste the billing model, and change one arrow until the sentence you say out loud matches the line. If you can't say the sentence, the line is decorative. Delete it.
Related posts
Frequently asked questions
Which line means inheritance?
A triangle arrow, written <|-- , with the arrowhead on the parent. Reversing it is a modeling bug the parser will not catch.
Why did my cardinality fail to parse?
An unquoted star is easy to misread. Write "1" --> "*" with both numbers quoted.
Is a class diagram an ERD?
No. Methods and inheritance belong here. Foreign keys and row counts belong on an ER diagram.