Developer Reference · Visual Documentation

Diagrams as
Code

A complete guide to generating, maintaining, and communicating architecture through diagrams — from context maps to async job flows.

Mermaid · PlantUML · C4
Rails · Sidekiq · Event Storming
Auto-generation · CI/CD
01 · Philosophy

Why Diagrams-as-Code?

Diagrams-as-code means your architecture documentation lives in your repository, diffs with your PRs, and never drifts out of sync because a developer forgot to update a Confluence page. When the code changes, the diagram changes — ideally automatically.

The core principle: a diagram you can't version control is a diagram you can't trust. Visio screenshots and draw.io exports rot the moment they're committed.

Treat diagrams like tests. They should live next to the code they describe, run in CI, and fail loudly when stale.

The Four Questions Every Diagram Answers

  1. What exists? — Context & component diagrams. The static inventory of your system.
  2. How does it connect? — Dependency graphs, class diagrams. Structural relationships.
  3. What happens at runtime? — Sequence diagrams, event flows. Dynamic behavior.
  4. How does it change? — State machines. How an entity moves through its lifecycle.
02 · Tooling

Choosing Your Format

Mermaid is the pragmatic default — native in GitHub, GitLab, Notion, and most documentation platforms. PlantUML is more powerful but requires a render server. Use the right tool for the audience.

🧜 Mermaid

Renders natively in GitHub/GitLab markdown. Best for sequence, flowcharts, state, ER, and basic C4. No install required for readers.

Native GitHub Easy syntax Web-first

🌱 PlantUML

More complete UML spec support. Better for C4 (via library), component diagrams, and anything Mermaid can't express. Needs a render server or local Java.

Full UML C4 library CI-friendly

📐 Structurizr

The official C4 model tool by Simon Brown. DSL-first, workspace-based, exports to PlantUML/Mermaid. Best for teams committing to C4 seriously.

C4 native DSL Multi-view

🔷 D2

Modern alternative with cleaner syntax than PlantUML. Auto-layout is excellent. Growing ecosystem. Good for architecture overviews.

Modern Auto-layout Go binary
Diagram Type Mermaid PlantUML Structurizr D2
Context (C4 L1) OK Best Native OK
Component OK Best Native Good
Sequence Best Good OK OK
Class/ER Good Best No OK
State Machine Good Good No OK
Event Storming Manual Manual No Manual
03 · C4 Model

The C4 Framework — Four Levels of Zoom

The C4 model, created by Simon Brown, is a hierarchical approach to documenting software architecture. Think of it like Google Maps: you zoom in for more detail, each level answering a more specific question.

  1. Level 1 — System Context: Your system and everything outside it. Users, external services, third-party APIs. The "30,000-foot view." Audience: anyone — business stakeholders, PMs, new engineers.
  2. Level 2 — Container: The deployable units inside your system — web app, background worker, database, cache, message broker. Audience: developers and architects.
  3. Level 3 — Component: Inside a single container, the major structural blocks — Rails engines, service objects, modules. Audience: developers working on that container.
  4. Level 4 — Code: Class diagrams, ER diagrams. Usually auto-generated. Audience: developers in that component. (Often skipped — the code IS the documentation at this level.)

In Rails apps, a "container" is typically: your Rails web process, Sidekiq worker process, PostgreSQL, Redis, and any external APIs you call. Start there for your L2 diagram.

C4 Context Diagram (Level 1) — Mermaid

%%{init: {'theme': 'base', 'themeVariables': {'primaryColor': '#2a6b8a'}}}%%
C4Context
  title System Context — MyApp

  %% Actors
  Person(user, "End User", "Uses the web interface")
  Person(admin, "Admin", "Manages system via back-office")

  %% Your system — the box
  System(myapp, "MyApp", "Rails monolith with background jobs")

  %% External systems
  System_Ext(stripe, "Stripe", "Payment processing")
  System_Ext(sendgrid, "SendGrid", "Transactional email")
  System_Ext(s3, "AWS S3", "File storage")

  %% Relationships
  Rel(user, myapp, "Uses", "HTTPS")
  Rel(admin, myapp, "Administers", "HTTPS")
  Rel(myapp, stripe, "Charges cards via", "HTTPS API")
  Rel(myapp, sendgrid, "Sends email via", "HTTPS API")
  Rel(myapp, s3, "Stores files in", "AWS SDK")

Mermaid's C4 support is still maturing. For the most complete C4 output, use PlantUML with the C4-PlantUML library, which gives you boundary boxes, proper icon support, and correct layout.

C4 Container Diagram (Level 2) — PlantUML

!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml

LAYOUT_WITH_LEGEND()
title Container Diagram — MyApp

Person(user, "User")
Person(admin, "Admin")

System_Boundary(myapp, "MyApp") {
  Container(web, "Rails App", "Ruby on Rails", "Handles HTTP, renders views, exposes API")
  Container(worker, "Sidekiq Workers", "Ruby/Sidekiq", "Processes background jobs")
  ContainerDb(db, "PostgreSQL", "PostgreSQL 15", "Primary data store")
  ContainerDb(cache, "Redis", "Redis 7", "Job queue, Rails cache, sessions")
}

Rel(user, web, "HTTPS")
Rel(web, db, "ActiveRecord", "TCP")
Rel(web, cache, "Enqueues jobs, reads cache", "Redis protocol")
Rel(worker, db, "Reads/writes", "TCP")
Rel(worker, cache, "Dequeues jobs", "Redis protocol")
04 · Context Diagrams

System Context — The High-Level View

Context diagrams answer "what is this system, and who/what does it talk to?" They're the entry point for any new team member and the diagram most useful for non-technical stakeholders.

Key Rules for Context Diagrams

Simple Flowchart Context in Mermaid (no C4 library needed)

flowchart TB
  %% Style definitions
  classDef system fill:#2a6b8a,color:#fff,stroke:#1a4b6a
  classDef external fill:#6a6a5a,color:#fff,stroke:#4a4a3a
  classDef person fill:#d4521a,color:#fff,stroke:#a43212

  User([👤 End User]):::person
  Admin([👤 Admin]):::person

  App["**MyApp**\nRails on Fly.io"]:::system

  Stripe[Stripe\nPayments]:::external
  SG[SendGrid\nEmail]:::external
  S3[AWS S3\nStorage]:::external
  Twilio[Twilio\nSMS]:::external

  User -->|HTTPS| App
  Admin -->|HTTPS| App
  App -->|REST API| Stripe
  App -->|SMTP/API| SG
  App -->|SDK| S3
  App -->|REST API| Twilio

Put context diagrams in your repo's root README.md or a top-level docs/architecture/ directory. GitHub renders Mermaid blocks natively in markdown — no image exports needed.

05 · Component Diagrams

Component & Dependency Diagrams

Component diagrams zoom into a single container and show its internal structure — services, modules, classes, and how they depend on each other. In Rails, this is the layer where you document your service objects, query objects, presenters, and engines.

Class Diagram in Mermaid

Use class diagrams to show domain models and their relationships, including inheritance, composition, and associations.

classDiagram
  class Order {
    +Integer id
    +String status
    +Decimal total_cents
    +place()
    +cancel()
    +refund()
  }

  class OrderItem {
    +Integer quantity
    +Decimal unit_price
    +subtotal() Decimal
  }

  class Product {
    +String name
    +String sku
    +Boolean in_stock?
  }

  class Customer {
    +String email
    +lifetime_value() Decimal
  }

  class PaymentProcessor {
    <<Service>>
    +charge(order) Result
    +refund(order) Result
  }

  Customer "1" --> "many" Order : places
  Order "1" *-- "many" OrderItem : contains
  OrderItem "many" --> "1" Product : references
  Order ..> PaymentProcessor : uses

Module/Service Dependency Graph (Mermaid)

Show how your Rails service objects, concerns, and modules depend on each other. This is invaluable for identifying circular dependencies and god objects.

flowchart LR
  %% Controllers
  subgraph Controllers
    OC[OrdersController]
    PC[PaymentsController]
  end

  %% Services
  subgraph Services
    OP[OrderPlacer]
    PR[PaymentRefunder]
    NS[NotificationService]
    IS[InventoryService]
  end

  %% Models
  subgraph Models
    O[Order]
    P[Product]
    C[Customer]
  end

  OC --> OP
  PC --> PR
  OP --> O
  OP --> IS
  OP --> NS
  PR --> O
  PR --> NS
  IS --> P
  NS --> C

Auto-Generating Rails Dependency Graphs

The rails-mermaid_erd gem auto-generates ER diagrams from your ActiveRecord models. Install it and add a Rake task:

# Gemfile (development group)
gem 'rails-mermaid_erd', group: :development
gem 'railroady', group: :development  # for class/model diagrams
# Generate ER diagram from your AR models
bundle exec rake erd:mermaid

# Railroady — generates SVG/dot class diagrams
bundle exec railroady -M | dot -Tsvg > docs/diagrams/models.svg
bundle exec railroady -C | dot -Tsvg > docs/diagrams/controllers.svg
06 · Static Diagrams

Static Diagrams — Structure & Data

Static diagrams describe things that exist, not things that happen. They answer: "What is the shape of this system?" Three types are most useful in Rails work.

Entity Relationship (ER) Diagrams

ER diagrams map your database schema. Mermaid's erDiagram type is excellent for this.

erDiagram
  CUSTOMERS {
    int id PK
    string email
    string name
    timestamp created_at
  }

  ORDERS {
    int id PK
    int customer_id FK
    string status
    int total_cents
    timestamp placed_at
  }

  ORDER_ITEMS {
    int id PK
    int order_id FK
    int product_id FK
    int quantity
    int unit_price_cents
  }

  PRODUCTS {
    int id PK
    string name
    string sku
    boolean active
  }

  CUSTOMERS ||--o{ ORDERS : places
  ORDERS ||--|{ ORDER_ITEMS : contains
  PRODUCTS ||--o{ ORDER_ITEMS : referenced_in

You can auto-generate Mermaid ER diagrams from your Rails schema using rails-mermaid_erd. Run it as part of your migration workflow so diagrams stay current.

State Machine Diagrams

Document AASM or state_machines-activerecord transitions explicitly. State diagrams make edge cases visible that are easy to miss in code.

stateDiagram-v2
  [*] --> pending : Order created

  pending --> payment_processing : User submits payment
  payment_processing --> paid : Payment succeeds
  payment_processing --> payment_failed : Payment declined

  paid --> fulfilling : Inventory confirmed
  paid --> refunded : Admin refunds

  fulfilling --> shipped : Label created
  shipped --> delivered : Carrier confirms
  shipped --> lost : 30-day timeout

  payment_failed --> pending : User retries
  payment_failed --> cancelled : Abandoned (48h)

  delivered --> [*]
  cancelled --> [*]
  refunded --> [*]

  note right of fulfilling : Triggers FulfillmentJob

Database Schema Visualization (rake task)

# lib/tasks/diagrams.rake
namespace :diagrams do
  desc "Generate Mermaid ER diagram from current schema"
  task erd: :environment do
    output = MermaidErd.generate
    File.write("docs/diagrams/erd.md", output)
    puts "ER diagram written to docs/diagrams/erd.md"
  end
end

# Hook into db:migrate automatically
if Rails.env.development?
  Rake::Task["db:migrate"].enhance do
    Rake::Task["diagrams:erd"].invoke
  end
end
07 · Runtime & Event Storming

Runtime Diagrams & Event Storming

Runtime diagrams capture what happens over time — the choreography of a request, the sequence of messages, the order of side effects. These are the diagrams developers actually reach for when debugging a bug they can't reproduce.

Sequence Diagrams

Sequence diagrams are the workhorse of runtime documentation. Use them for API request flows, webhook handlers, multi-service orchestration, and anywhere the order of operations matters.

sequenceDiagram
  autonumber
  actor U as User
  participant C as OrdersController
  participant OP as OrderPlacer
  participant DB as PostgreSQL
  participant Q as Redis/Sidekiq Queue

  U->>C: POST /orders
  C->>OP: call(order_params)

  OP->>DB: BEGIN transaction
  OP->>DB: INSERT order
  OP->>DB: UPDATE inventory (lock rows)

  alt Inventory available
    OP->>DB: COMMIT
    OP->>Q: Enqueue PaymentJob(order_id)
    OP->>C: Success(order)
    C->>U: 201 Created
  else Out of stock
    OP->>DB: ROLLBACK
    OP->>C: Failure(:out_of_stock)
    C->>U: 422 Unprocessable
  end

Event Storming in Mermaid

Event storming is a workshop technique, but you can capture its output as diagrams. The key notation: domain events (things that happened, past tense), commands (what triggered them), and read models (what queries consume them).

Use a horizontal timeline flowchart to represent the event flow:

flowchart LR
  %% Color coding via classDef mimics sticky notes
  classDef event fill:#d4521a,color:#fff,stroke:none
  classDef command fill:#2a6b8a,color:#fff,stroke:none
  classDef policy fill:#7a5a9a,color:#fff,stroke:none
  classDef readmodel fill:#2a7a4a,color:#fff,stroke:none
  classDef actor fill:#c8a000,color:#000,stroke:none

  %% Commands (blue)
  PlaceOrder[Place Order]:::command
  ProcessPayment[Process Payment]:::command
  FulfillOrder[Fulfill Order]:::command

  %% Events (orange)
  OrderPlaced([Order Placed]):::event
  PaymentProcessed([Payment Processed]):::event
  PaymentFailed([Payment Failed]):::event
  OrderFulfilled([Order Fulfilled]):::event
  OrderShipped([Order Shipped]):::event

  %% Policies (purple) — "when X then Y"
  Policy1{When OrderPlaced\nthen start payment}:::policy
  Policy2{When PaymentProcessed\nthen begin fulfillment}:::policy

  %% Read models (green)
  OrderDash[(Order Dashboard)]:::readmodel

  %% Flow
  PlaceOrder --> OrderPlaced
  OrderPlaced --> Policy1
  Policy1 --> ProcessPayment
  ProcessPayment --> PaymentProcessed
  ProcessPayment --> PaymentFailed
  PaymentProcessed --> Policy2
  Policy2 --> FulfillOrder
  FulfillOrder --> OrderFulfilled
  OrderFulfilled --> OrderShipped
  OrderPlaced --> OrderDash
  PaymentProcessed --> OrderDash

Event storming diagrams are most useful when created collaboratively. Use the Mermaid format to capture the session output, not to run the session itself. Miro or physical stickies for the workshop; Mermaid for the permanent record.

08 · Async Operations

Representing Sidekiq & Async Jobs

Asynchronous operations are where most Rails diagrams fall apart — they're invisible in standard request/response flows. The trick is making the queue explicit and showing the temporal gap between enqueue and execution.

Key Conventions for Async Flows

Sidekiq Job Flow — Sequence Diagram

sequenceDiagram
  participant Web as Rails Web Process
  participant Redis as Redis Queue
  participant SK as Sidekiq Worker
  participant Stripe as Stripe API
  participant DB as PostgreSQL
  participant Email as SendGrid

  note over Web,Redis: Enqueue phase (synchronous, fast)
  Web->>Redis: LPUSH sidekiq:critical PaymentJob{order_id: 42}
  Redis-->>Web: OK (enqueued)
  Web-->>Web: Return 201 to user immediately

  note over Redis,SK: Some time later...

  SK->>Redis: BRPOP sidekiq:critical
  Redis-->>SK: PaymentJob payload

  note over SK,DB: Execution phase (async, can fail + retry)
  SK->>DB: SELECT * FROM orders WHERE id=42 FOR UPDATE
  SK->>Stripe: POST /v1/charges
  Stripe-->>SK: {status: "succeeded"}
  SK->>DB: UPDATE orders SET status='paid'
  SK->>Email: POST /v3/mail/send (receipt)

  rect rgba(212,82,26,0.08)
    note over SK,Redis: On failure: Sidekiq retries with exponential backoff
    SK->>Redis: LPUSH sidekiq:retry (job + retry_count + next_run_at)
  end

Job Dependency DAG

When jobs trigger other jobs, document the dependency graph. This prevents circular chains and makes the async architecture explicit.

flowchart TD
  classDef job fill:#1a1a1a,color:#e8dcc8,stroke:#3a3a3a
  classDef trigger fill:#2a6b8a,color:#fff,stroke:none
  classDef queue stroke:#d4521a,stroke-width:2px

  %% Trigger
  WebReq[HTTP Request\nPOST /orders]:::trigger

  %% Job graph
  J1[PaymentJob\nqueue: critical]:::job
  J2[FulfillmentJob\nqueue: default]:::job
  J3[EmailReceiptJob\nqueue: low]:::job
  J4[InventoryUpdateJob\nqueue: default]:::job
  J5[AnalyticsTrackJob\nqueue: low]:::job

  WebReq -->|enqueues| J1
  J1 -->|on success: enqueues| J2
  J1 -->|always enqueues| J3
  J2 -->|enqueues| J4
  J1 -->|enqueues| J5

  %% Dead letter
  DLQ[(Dead Letter Queue)]
  J1 -.->|after 25 retries| DLQ
  J2 -.->|after 25 retries| DLQ

Documenting Scheduled Jobs (Cron)

flowchart LR
  classDef cron fill:#7a5a9a,color:#fff,stroke:none
  classDef job fill:#1a1a1a,color:#e8dcc8,stroke:#3a3a3a

  C1[⏰ Daily 2am UTC]:::cron
  C2[⏰ Every 15min]:::cron
  C3[⏰ Weekly Mon 6am]:::cron

  J1[ExpiredSessionCleanupJob]:::job
  J2[CacheWarmingJob]:::job
  J3[WeeklyReportJob]:::job

  C1 --> J1
  C2 --> J2
  C3 --> J3

For complex Sidekiq setups, document your queue priorities and concurrency configuration explicitly in a diagram. Show how many threads each queue gets and why — this is rarely documented and constantly confusing for new team members.

09 · Auto-generation

Keeping Diagrams Fresh — Auto-generation

Diagrams are only useful if they're accurate. Manual updates don't scale. Here's how to wire up your Rails app so diagrams regenerate automatically when the code changes.

Gems Worth Installing

GemGeneratesFormat
rails-mermaid_erd ER diagram from AR models Mermaid markdown
railroady Model & controller class diagrams SVG via Graphviz
annotate Schema comments on models/specs Ruby comments
bullet N+1 warnings (feeds into docs) Logs

Rake Task: Regenerate All Diagrams

# lib/tasks/diagrams.rake
namespace :diagrams do
  DIAGRAM_DIR = Rails.root.join("docs/diagrams")

  desc "Regenerate all architecture diagrams"
  task all: [:erd, :models, :controllers]

  task erd: :environment do
    puts "→ Generating ER diagram..."
    output = MermaidErd.generate
    File.write(DIAGRAM_DIR.join("erd.md"), output)
  end

  task models: :environment do
    puts "→ Generating model diagram..."
    sh "bundle exec railroady -M --transitive |
         dot -Tsvg -o #{DIAGRAM_DIR}/models.svg"
  end

  task controllers: :environment do
    puts "→ Generating controller diagram..."
    sh "bundle exec railroady -C |
         dot -Tsvg -o #{DIAGRAM_DIR}/controllers.svg"
  end

  # Hook ER generation into migrations
  Rake::Task["db:migrate"].enhance { Rake::Task["diagrams:erd"].invoke }
end

Git Hook: Regenerate on Schema Change

#!/bin/bash
# .git/hooks/post-merge (also add to post-checkout)

CHANGED=$(git diff HEAD@{1} HEAD --name-only)

if echo "$CHANGED" | grep -q "db/schema.rb"; then
  echo "🔄 Schema changed — regenerating diagrams..."
  bundle exec rake diagrams:erd
  git add docs/diagrams/erd.md
  git commit -m "chore: regenerate ER diagram after schema change" --no-verify
fi

Guard Integration (watch for file changes in dev)

# Guardfile
guard :rake, task: 'diagrams:erd' do
  watch('db/schema.rb')
end

guard :rake, task: 'diagrams:models' do
  watch(%r{^app/models/.+\.rb$})
end

CI/CD: GitHub Actions

# .github/workflows/diagrams.yml
name: Regenerate Diagrams
on:
  push:
    paths: ['db/schema.rb', 'app/models/**']

jobs:
  diagrams:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Install Graphviz
        run: sudo apt-get install -y graphviz

      - name: Regenerate diagrams
        run: bundle exec rake diagrams:all

      - name: Commit updated diagrams
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "chore: auto-regenerate architecture diagrams [skip ci]"
          file_pattern: 'docs/diagrams/**'
10 · Workflow

A Practical Documentation Workflow

Here's a concrete workflow for adding diagrams to a feature from scratch.

  1. New feature branch: Before writing code, sketch the C4 L2 container view and a sequence diagram for the happy path. Writing the diagram first forces architectural decisions up front. This is design-by-diagram.
  2. During implementation: Keep the sequence diagram as a living doc in the PR description. Update it as the implementation diverges from the plan. (It will. That's fine.)
  3. New model or migration? Run rake diagrams:erd and commit the updated ER diagram in the same PR as the migration. Reviewers see schema changes and diagram together.
  4. Async job added? Add it to the job dependency graph. If it's a new Sidekiq class, add a sequence diagram showing its enqueue/execute flow.
  5. State machine change? Update the state diagram in the same commit as the AASM/state_machines change. Never let these drift.
  6. PR review: Add docs/diagrams/ as a required review path in CODEOWNERS so architecture changes get architectural review.
  7. Post-merge: CI auto-regenerates any auto-generated diagrams (ER, class diagrams) and commits them back. Hand-maintained diagrams (context, sequence) are committed manually.

Directory Structure

docs/
└── diagrams/
    ├── README.md              # Index with embedded diagrams
    ├── context.md             # C4 L1 — system context (manual)
    ├── containers.md          # C4 L2 — containers (manual)
    ├── erd.md                 # ER diagram (auto-generated)
    ├── models.svg             # Class diagram (auto-generated)
    ├── state-machines/
    │   ├── order.md           # Order state machine
    │   └── subscription.md   # Subscription state machine
    ├── sequences/
    │   ├── checkout-flow.md   # Checkout sequence diagram
    │   └── webhook-flow.md    # Stripe webhook handling
    └── jobs/
        └── job-dependencies.md # Sidekiq job graph

Add a docs/diagrams/README.md that embeds all your Mermaid diagrams inline. GitHub renders them beautifully, giving you a visual architecture dashboard that any team member can browse without special tooling.


The Golden Rule

A diagram that requires manual updates will become wrong.
Auto-generate everything you can. Version control everything else.
The best diagram is the one that exists.