Why this session exists
Sessions 1-3 drew how Nova FLOWS - boxes, arrows, sequences. But every data conversation eventually hits the same question: "wait, what exactly is IN the warehouse, and how do the tables relate?" That answer usually lives in someone's head, a stale wiki page, or a 40-tab spreadsheet. Today it becomes twelve lines of text: erDiagram for the data model, classDiagram for the pipeline code that feeds it. These two are the most copy-pasted diagram types in data teams, and the syntax is smaller than it looks.
ER diagrams - the warehouse on one page 9 min live
An erDiagram is built from sentences: ENTITY cardinality--cardinality ENTITY : verb. Learn to read that one line shape and you can read any data model ever written in mermaid.
LiveEntities and the relationship sentence3 min▶
Entities are THINGS your business tracks - written in CAPS by convention, one per table (or one per business concept, if you're modeling before the tables exist). A relationship line connects two of them, with a cardinality pair on each end and a verb label after the colon.
erDiagram
CUSTOMER ||--o{ ORDER : places
The middle of the line picks the relationship style: -- is identifying (solid line - the child cannot exist without the parent, like an order line without its order) and .. is non-identifying (dashed line - the entities are related but independently viable). The full cardinality alphabet:
| Left of -- | Right of -- | Meaning |
|---|---|---|
|o | o| | zero or one |
|| | || | exactly one |
}o | o{ | zero or more |
}| | |{ | one or more |
Notice the mirror: the crow's foot ({ or }) always points AT the entity it describes, so the same "many" symbol is written }o on the left end and o{ on the right end. If typing symbols feels cryptic, mermaid also accepts word aliases like one or more and zero or many - handy while learning, symbols once fluent.
LiveAttributes, types, and keys3 min▶
An entity earns its place on the diagram when you show WHAT it holds. Open a block with braces and list type name pairs, one per line. Tag key columns with PK, FK, or UK, and hang a comment in quotes at the end of any line - the single best place to record data gotchas.
erDiagram
CUSTOMER ||--o{ SUBSCRIPTION : signs
CUSTOMER {
int customer_id PK
string email UK "lowercased at ingestion"
string full_name
date signed_up_on
}
SUBSCRIPTION {
int subscription_id PK
int customer_id FK
string plan
date ends_on "null while active"
}
- Types are free text -
int,string,timestamp,decimal, whatever your warehouse calls them. Mermaid doesn't validate; your reviewers do. - PK / FK / UK = primary, foreign, unique key. A column can carry several, comma-separated:
int customer_id PK, FK- exactly what bridge tables need. - Quoted comments render right on the diagram. "null while active" has saved more analysts than any wiki page.
LiveNova's core warehouse model - the whole thing3 min▶
Here is Nova's core model: five entities, five relationships, every key marked. This is the artifact Demo 1 builds line by line - read it top to bottom and notice it narrates itself: customers sign subscriptions, place orders, generate events; campaigns target customers and get attribution credit on orders.
erDiagram
CUSTOMER ||--o{ SUBSCRIPTION : signs
CUSTOMER ||--o{ ORDER : places
CUSTOMER ||--o{ EVENT : generates
CAMPAIGN |o..o{ ORDER : "gets credit for"
CAMPAIGN }o..o{ CUSTOMER : targets
CUSTOMER {
int customer_id PK
string email UK "lowercased at ingestion"
date signed_up_on
}
SUBSCRIPTION {
int subscription_id PK
int customer_id FK
string plan
date ends_on "null while active"
}
EVENT {
string event_id PK
int customer_id FK
string event_type
timestamp occurred_at
}
ORDER {
int order_id PK
int customer_id FK
int campaign_id FK "null for organic orders"
decimal amount
}
CAMPAIGN {
int campaign_id PK
string channel
date launched_on
}
Note the relationship styles doing real work: customer-to-order is identifying (--, an order is meaningless without its customer), while campaign links are non-identifying (.., orders exist happily without a campaign - that's what "null for organic orders" is telling you, in two places that now agree).
Self-studyHow much model belongs on one diagram?2 min read▶
Real warehouses have hundreds of tables; ER diagrams have a working budget of roughly 8-12 entities before they stop being read. Three habits keep them useful:
- One diagram per domain - a "customer domain" ERD, a "billing domain" ERD - linked from a small index diagram, exactly like we split Nova's flowcharts in Session 2.
- Attributes only where they earn rent. Show full blocks for the 3-4 entities under discussion; leave the rest as bare named boxes for context.
- Model the business, not the database dump. Skip audit columns and ETL bookkeeping unless the diagram is ABOUT them. The reader wants the shape, not the DDL.
Reading and writing cardinality 5 min live
Cardinality symbols are where every beginner stalls, and one grammar trick fixes it forever: the symbol pair sitting NEXT TO an entity answers "how many of THIS entity per one of the other?"
LiveThe three patterns that cover 90% of models3 min▶
Almost every real relationship is one of three shapes. Learn them as idioms and you'll write them without thinking:
erDiagram
TEAM ||--|{ MEMBER : has
CUSTOMER |o--o| LOYALTY-CARD : holds
CUSTOMER ||--o{ CAMPAIGN-TOUCH : receives
CAMPAIGN ||--o{ CAMPAIGN-TOUCH : sends
CAMPAIGN-TOUCH {
int customer_id PK, FK
int campaign_id PK, FK
date touched_on
}
- One-to-many - the workhorse.
TEAM ||--|{ MEMBER: a team has one or more members, a member belongs to exactly one team. Ask "can it exist empty?" to pick|{vso{. - Optional one-to-one -
|o--o|. Both sides may exist alone; at most one pairing. Loyalty cards, parking spots, profile extensions. - Many-to-many via bridge - customers receive many campaigns, campaigns hit many customers. Warehouses don't store many-to-many directly: a bridge entity (CAMPAIGN-TOUCH) with a composite key (
PK, FKtwice) resolves it into two clean one-to-manys - and usually grows useful facts of its own, liketouched_on.
The optionality argument that saved a quarter. A team modeled orders-to-campaigns as mandatory (||) in their tracking plan. An analyst reading the ERD asked "so organic orders are... impossible?" That one symbol flushed out a broken assumption in the attribution logic BEFORE the quarter's numbers shipped wrong. Cardinality review is cheap; restatements are not.
Self-studyIdentifying or not? The two-question test1 min read▶
Choosing -- vs .. trips people up because it FEELS philosophical. It isn't. Ask two questions about the child entity:
- Can it exist without the parent? An EVENT without a CUSTOMER is noise; that relationship is identifying (
--). An ORDER without a CAMPAIGN is just an organic order; non-identifying (..). - Is the parent's key part of the child's identity? If the foreign key sits inside the primary key (like both columns of CAMPAIGN-TOUCH), that's identifying by definition.
When still unsure, default to non-identifying - it's the weaker claim, and a reviewer can strengthen it with evidence.
Class diagrams - when structure is code 6 min live
ER diagrams describe what you STORE; class diagrams describe what you BUILD - the connectors, checks, and pipeline objects around the warehouse. Same idea, richer arrows: six relationship types instead of one line style.
LiveClasses, members, visibility3 min▶
A class block lists attributes and methods - methods are just members with (). Prefix each member with a visibility marker: + public, - private, # protected, ~ package. Return types go AFTER the method: +run() CheckResult.
classDiagram
class QualityCheck {
+String name
+String severity
-int failCount
#bool strictMode
~String ownerTeam
+run(batch) CheckResult
-log(message) void
}
- Visibility is documentation of intent:
+is the API your teammates call,-is internals free to change,#is the contract with subclasses. - Suffixes for the fine print:
$marks a static member,*an abstract one -+read() Batch*reads as "subclasses must implement this". - Don't inventory every field. A diagram class with 30 members is a screenshot of the code, not a model of it. Show what a caller or subclass needs to know.
LiveThe six arrows + Nova's ingestion SDK3 min▶
Class relationships are drawn with six arrows, and the two diamonds are the ones interviews ask about. Composition (*--) means the part LIVES AND DIES with the whole; aggregation (o--) means the whole USES parts that outlive it.
| You type | Relationship | Read it as |
|---|---|---|
<|-- | inheritance | "is a" |
*-- | composition | "owns, part dies with whole" |
o-- | aggregation | "has, part outlives whole" |
--> | association | "knows about" |
..> | dependency | "uses briefly" |
..|> | realization | "implements the interface" |
Here is Nova's ingestion SDK - the code from Sessions 2-3's flowcharts, now as structure. Multiplicities ride the relationship in quotes, labels after a colon:
classDiagram
class Connector {
+String name
#int batchSize
+connect() bool
+read() Batch*
}
class EventStreamConnector {
+String topic
+read() Batch
}
class CrmExportConnector {
+String dropPath
+read() Batch
}
class Pipeline {
-List~Connector~ sources
+run() RunReport
}
Connector <|-- EventStreamConnector
Connector <|-- CrmExportConnector
Pipeline o-- "1..*" Connector : reads from
Pipeline *-- RunLog : owns
Read the diamonds: connectors are aggregated (o--) because they're registered from outside and survive any single pipeline run; the RunLog is composed (*--) because it is created by the run and meaningless without it.
Self-studyGenerics, annotations, namespaces, notes3 min read▶
Four refinements for when the SDK diagram grows up:
- Generics with tildes: angle brackets clash with mermaid's arrow syntax, so
List<Connector>is writtenList~Connector~. Works in class names too:class Queue~T~. - Annotations mark a class's kind on the first line of its body:
<<interface>>,<<abstract>>,<<service>>. Pair<<interface>>with realization arrows (..|>) and your "implements" story draws itself. - Namespaces group classes into labeled boxes:
namespace Ingestion { class Connector }- the class-diagram cousin of Session 2's subgraphs. - Notes pin commentary:
note for Connector "one subclass per source system".
The highest-value class diagram in most data teams is the connector/check/writer interface sheet: the three interfaces marked <<interface>>, concrete classes hanging off them with ..|>. New joiners see in ten seconds where their new source plugs in - and which three methods they owe.
Build the Nova warehouse ERD ★ 9 min · follow along
We rebuild Part 1's full Nova model from an empty editor, entity by entity - the exact order you'd use for any real model: relationships first, attributes second, comments last.
Open mermaid.live, type erDiagram, then the skeleton sentence: CUSTOMER ||--o{ ORDER : places. Two boxes, one crow's foot. Say it out loud both ways using the decoder rule.
Add the other customer relationships: CUSTOMER ||--o{ SUBSCRIPTION : signs and CUSTOMER ||--o{ EVENT : generates. CUSTOMER is now visibly the hub - correct, it's the business's center of gravity.
Add campaigns with DASHED lines: CAMPAIGN |o..o{ ORDER : "gets credit for" and CAMPAIGN }o..o{ CUSTOMER : targets. Multi-word labels need quotes - same quoting rule as Session 1.
Furnish CUSTOMER: open a brace block, add int customer_id PK, string email UK, date signed_up_on. Watch the box grow rows with key badges.
Furnish ORDER, and put the gotcha in writing: int campaign_id FK "null for organic orders". That one comment is the difference between a diagram and documentation.
Fill the remaining three entities from Part 1's block, then compare. Commit yours to a repo README in a ```mermaid fence - Nova's data model now lives where the code lives.
Diagram YOUR schema ★ 6 min · build your own
Nova is rehearsal; your warehouse is the performance. Pick one table cluster you actually query and give it the same treatment.
Choose 3-5 related tables you know well - a fact table and its dimensions is the classic pick. Resist the urge to do the whole warehouse.
Write the relationship sentences FIRST, no attributes. Read each one aloud in both directions - this is where you'll discover you're not actually sure whether that join is one-to-one or one-to-many.
Add attribute blocks for the two most-queried tables only: keys tagged, types honest, and at least one quoted comment recording a gotcha you know ("timezone is UTC", "soft deletes only").
Check every many-to-many: if you drew }o--o{ directly, find (or propose) the bridge table - it almost always exists in the warehouse already.
Send the mermaid.live link to the teammate who knows this data best with one question: "what's wrong?" Their correction IS the documentation improving.
Teams that keep a small ERD at the top of each dbt domain folder cut onboarding questions dramatically - not because the diagram is complete, but because it names the entities and joins, which is 80% of what "how does this data work?" actually means.
Class-diagram the ingestion SDK ★ 4 min · follow along
Four classes, four minutes: Nova's connector family from Part 3, rebuilt to prove the class syntax is smaller than it looks.
Start with classDiagram, declare class Connector with two members: +connect() bool and +read() Batch*. The trailing * says abstract - subclasses owe an implementation.
Add the two concrete sources and wire inheritance: Connector <|-- EventStreamConnector, Connector <|-- CrmExportConnector. Arrowhead points at the parent.
Add Pipeline with -List~Connector~ sources - tildes, not angle brackets, for generics.
Finish with the two diamonds: Pipeline o-- "1..*" Connector : reads from and Pipeline *-- RunLog : owns. Say the lifecycle rule for each diamond out loud - if you can, you've got this session.
Try it yourself - this week ◐ 30-45 min total
- Finish your Demo 2 ERD to 5-8 entities and commit it to the repo or wiki where that data is actually discussed. One quoted comment per entity, minimum.
- Cardinality drill: write the relationship sentence for five real pairs in your world (user/session, invoice/line, model/version...) - symbols only, then check by reading both directions aloud.
- Rebuild Nova's ERD from memory, then diff against Part 1. Whatever you forgot - usually the
..vs--choice - is the review topic. - Class-diagram one real piece of your pipeline code (3-5 classes). If you can't decide between
*--ando--somewhere, that's a genuine design question - bring it to a teammate. - Self-study pass: add
<<interface>>annotations and one namespace to your class diagram, and skim the official pages for both diagram types. - Bring to Session 5: one real project or roadmap you're currently tracking in a spreadsheet. It's about to become a gantt chart.
Official docs covered
This session teaches the working content of the entity relationship diagram and class diagram pages of the official docs at mermaid.js.org (mermaid 11.x).
Three questions before you go 🎯 ◐ 90 seconds
1 · CUSTOMER ||--o{ ORDER : places - which reading is correct?
Read the symbol nearest each entity: || sits by CUSTOMER (each order gets exactly one), o{ sits by ORDER (each customer gets zero or more). The decoder rule, applied.
2 · How do you mark customer_id as a foreign key inside an entity's attribute block?
Attribute lines read type, name, key(s), optional quoted comment - in that order. Multiple keys are comma-separated: int customer_id PK, FK.
3 · Pipeline *-- RunLog uses the filled diamond. What is it claiming?
*-- is composition - strong lifecycle ownership. The hollow diamond o-- is aggregation, the "part outlives the whole" claim we used for connectors.