learn-mermaid-with-phoebe / Session 4 of 6
Learn Mermaid with Phoebe · Session 4 of 6

Schemas that speak: ER and class diagrams

Your warehouse has a shape, and today you learn to write it down: entity-relationship diagrams for the data, class diagrams for the code that moves it. By the end, Nova's whole core model fits on one page - and reads out loud like a sentence.

🟡 Moderate Data & AI teams 45 min live + self-study depth
0-3 · Recap 3-22 · Concepts (live cards) 22-41 · Three demos 41-45 · Q&A
Part 0

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.

Live - presented in session Self-study - read after class ★ Try it now Every diagram on this page is REAL mermaid, rendered live
★ What you walk out with today The full ER vocabulary - entities, crow's foot cardinalities, identifying vs non-identifying relationships, typed attributes with PK/FK/UK keys - plus working classDiagram basics: members, visibility, and the six relationship arrows. And the big artifact: Nova's core warehouse model (CUSTOMER, SUBSCRIPTION, EVENT, ORDER, CAMPAIGN) on one page, ready to drop into your own repo as a template.
Part 1 · covers "Entity Relationship Diagrams" core syntax

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
|oo|zero or one
||||exactly one
}oo{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.
Comment the traps, not the obvious Nobody needs "the customer's email" explained. They desperately need "lowercased at ingestion" and "null for organic orders". Reserve quoted comments for the facts that break queries.
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.
Part 2 · covers cardinality + identifying relationships in depth

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?"

The four symbol pairs |o o| zero or one optional, at most one || || exactly one mandatory single }o o{ zero or more optional many }| |{ one or more mandatory many Reading one line, both ways CUSTOMER ORDER || o{ Symbol NEAREST an entity describes how many of THAT entity you get. || by CUSTOMER: each order has exactly one customer. o{ by ORDER: each customer places zero or more orders. The crow's foot always points AT the "many" entity - same symbol, mirrored spelling per side. Say every relationship out loud in both directions before you commit it. If either sentence sounds wrong, the symbols are wrong.
🔍 Click to zoom - the crow's foot decoder. Four pairs, one reading rule, zero mystery.
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 |{ vs o{.
  • 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, FK twice) resolves it into two clean one-to-manys - and usually grows useful facts of its own, like touched_on.
Real world

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.

Part 3 · covers "Class diagrams" basics + relationships

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 typeRelationshipRead 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.

Composition *-- Pipeline RunLog Filled diamond: strong ownership. Delete the pipeline run and its log goes with it. Part dies with whole. Aggregation o-- Pipeline Connector Hollow diamond: shared use. Retire the pipeline and connectors live on, serving others. Part outlives whole. Memory hook: the FILLED diamond has swallowed the part - it isn't coming back out.
🔍 Click to zoom - composition vs aggregation, the two diamonds that carry lifecycle meaning.
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 written List~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".
Real world

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.

Demo 1 of 3 · the session's big artifact

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.

Layout looking tangled? ER layout follows declaration order. Declare the hub entity's relationships first and the diagram usually organizes itself around it. Still messy? Fewer entities per diagram beats fighting the layout engine.
Demo 2 of 3

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.

Real world

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.

Demo 3 of 3

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.

Homework

Try it yourself - this week ◐ 30-45 min total

Source material

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).

✓ER: entities, relationships, attributesPart 1 - the relationship sentence, typed attribute blocks, PK/FK/UK, quoted comments
✓ER: cardinality + identifying relationshipsParts 1-2 - full crow's foot table, -- vs .., the three everyday patterns
✓Class: definition, members, visibilityPart 3 - + - # ~, return types, $ static, * abstract
✓Class: relationships + multiplicityPart 3 - all six arrows, quoted multiplicities, labels
◐Class: generics, annotations, namespaces, notesself-study card - working intro; styling and interaction stay with the docs
Check yourself

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.

Session 4 cheat sheet · pin this

The ER sentenceENTITY sym--sym ENTITY : verb. Say it aloud both directions before committing it.
Cardinality pairs|o zero-or-one · || exactly-one · }o zero-or-more · }| one-or-more. Crow's foot points at the "many" side.
Line style-- identifying (child can't exist without parent) · .. non-identifying (independent). Unsure? Default to ..
Attributestype name KEY "comment" inside braces. PK / FK / UK, comma-separated for composites. Comment the traps.
Many-to-manyNever draw }o--o{ directly - resolve through a bridge entity with a composite PK, FK key pair.
Class members+ public · - private · # protected · ~ package. Return type after method; $ static, * abstract.
Class arrows<|-- is-a · *-- owns (dies with) · o-- has (outlives) · --> knows · ..> uses · ..|> implements.
Nova so farFlow (S1-2), runtime (S3), and now the data model: 5 entities + the ingestion SDK. Next: the plan to build it.