C4 Model architecture diagram builder on top of Yog.
Choreo.C4 lets you document software architecture using the
C4 model — a simple hierarchical approach
for visualising software architecture at different levels of abstraction.
The four levels
- L1 System Context — people and software systems
- L2 Containers — applications and data stores inside a system
- L3 Components — building blocks inside a container
- L4 Code — classes / interfaces (delegate to
Choreo.UML)
When to use
Use Choreo.C4 when you need to communicate architecture to both
technical and non-technical stakeholders. The zoom levels map directly
to the C4 abstraction levels, so you can generate Context, Container,
and Component diagrams from a single model.
Quick Start
c4 =
Choreo.C4.new()
|> Choreo.C4.add_person(:customer, label: "Customer",
description: "A user of the bank")
|> Choreo.C4.add_software_system(:banking, label: "Internet Banking",
description: "Allows customers to check balances", scope: :in)
|> Choreo.C4.add_software_system(:mainframe, label: "Mainframe",
description: "Stores core banking info", scope: :out)
|> Choreo.C4.add_relationship(:customer, :banking,
label: "Views balances using")
|> Choreo.C4.add_relationship(:banking, :mainframe,
label: "Gets account info from")
# L2 — Containers inside the in-scope system
|> Choreo.C4.add_container(:web_app, label: "Web App",
technology: "JavaScript, React", parent: :banking)
|> Choreo.C4.add_container(:api, label: "API",
technology: "Elixir, Phoenix", parent: :banking)
|> Choreo.C4.add_container(:db, label: "Database",
technology: "Postgres", parent: :banking)
|> Choreo.C4.add_relationship(:customer, :web_app, label: "Uses")
|> Choreo.C4.add_relationship(:web_app, :api, label: "Makes API calls to")
|> Choreo.C4.add_relationship(:api, :db, label: "Reads from and writes to")
|> Choreo.C4.add_relationship(:api, :mainframe, label: "Makes RPC calls to")
# L3 — Components inside a container
|> Choreo.C4.add_component(:signin, label: "Sign In Controller",
technology: "Phoenix Controller", parent: :api)
|> Choreo.C4.add_component(:accounts, label: "Accounts Context",
technology: "Domain logic", parent: :api)
|> Choreo.C4.add_relationship(:signin, :accounts, label: "Uses")
# Generate diagrams at different zoom levels
context = Choreo.View.zoom(c4, level: 0)
container = Choreo.View.zoom(c4, level: 1)
component = Choreo.View.zoom(c4, level: 2)
dot = Choreo.C4.to_dot(c4)
mermaid = Choreo.C4.to_mermaid(c4)Analysis
# Find containers with no relationships
Choreo.C4.Analysis.orphan_nodes(c4)
# Validate structural soundness
Choreo.C4.Analysis.validate(c4)
# Find missing descriptions
Choreo.C4.Analysis.missing_descriptions(c4)
# Find missing technology labels
Choreo.C4.Analysis.missing_technology(c4)
Summary
Functions
Defines a cluster (subgraph) for grouping nodes visually.
Adds a component (building block inside a container).
Adds a container (application or data store within a software system).
Adds a person (user, actor, or role).
Adds a relationship (directed edge) between two C4 elements.
Adds a software system.
Returns all children of a given parent node.
Clears the explicit scope and removes all scope: :in tags.
Returns all edges as {from, to, weight} tuples.
Returns all edges with their metadata as {from, to, weight, meta} tuples.
Creates a new empty C4 model.
Returns all node IDs in the C4 model.
Returns all nodes of a given C4 type.
Returns the parent ID of a node, or nil.
Returns the scope node ID, or nil.
Sets the in-scope node for drill-down diagrams.
Returns a theme for Choreo.C4 diagrams.
Renders the C4 model to DOT format.
Returns the raw Yog.Multi.Graph struct underpinning the model.
Renders the C4 model to Mermaid.js flowchart syntax.
Types
@type t() :: %Choreo.C4{ clusters: %{required(String.t()) => map()}, edge_meta: %{optional(Yog.Multi.Graph.edge_id()) => map()}, graph: Yog.Multi.Graph.t(), scope: Yog.node_id() | nil, strict: boolean() }
Functions
Defines a cluster (subgraph) for grouping nodes visually.
In C4 diagrams clusters are often used to group containers by their parent software system or components by their parent container.
Options
:parent(String.t/0) - Parent cluster name for nesting.:label(String.t/0) - Display label for the cluster.:style(String.t/0) - Visual style.:fillcolor(String.t/0) - Background color override.:color(String.t/0) - Border color override.
Examples
iex> c4 = Choreo.C4.new() |> Choreo.C4.add_cluster("banking", label: "Internet Banking")
iex> c4.clusters["cluster_banking"].label
"Internet Banking"
@spec add_component(t(), Yog.node_id(), keyword()) :: t()
Adds a component (building block inside a container).
Options
:parent- Parent container node ID.:label(String.t/0) - Display label for the node.:description(String.t/0) - Longer description shown as tooltip.:technology(String.t/0) - Technology stack (e.g. 'Elixir, Phoenix').:shape(atom/0) - Shape override for DOT output.:fillcolor(String.t/0) - Background color override.:fontcolor(String.t/0) - Font color override.:style(String.t/0) - Style override.:penwidth- Border thickness override.:image(String.t/0) - Image/icon path or URL override.
Examples
iex> c4 = Choreo.C4.new()
...> |> Choreo.C4.add_software_system(:banking)
...> |> Choreo.C4.add_container(:api, parent: :banking)
...> |> Choreo.C4.add_component(:auth, label: "Auth Controller", technology: "Phoenix", parent: :api)
iex> :auth in Choreo.C4.nodes(c4)
true
iex> Map.get(c4.graph.nodes, :auth).node_type
:component
iex> Map.get(c4.graph.nodes, :auth).parent
:api
@spec add_container(t(), Yog.node_id(), keyword()) :: t()
Adds a container (application or data store within a software system).
Options
:parent- Parent software system node ID.:label(String.t/0) - Display label for the node.:description(String.t/0) - Longer description shown as tooltip.:technology(String.t/0) - Technology stack (e.g. 'Elixir, Phoenix').:shape(atom/0) - Shape override for DOT output.:fillcolor(String.t/0) - Background color override.:fontcolor(String.t/0) - Font color override.:style(String.t/0) - Style override.:penwidth- Border thickness override.:image(String.t/0) - Image/icon path or URL override.
Examples
iex> c4 = Choreo.C4.new()
...> |> Choreo.C4.add_software_system(:banking)
...> |> Choreo.C4.add_container(:api, label: "API", technology: "Phoenix", parent: :banking)
iex> :api in Choreo.C4.nodes(c4)
true
iex> Map.get(c4.graph.nodes, :api).node_type
:container
iex> Map.get(c4.graph.nodes, :api).parent
:banking
@spec add_person(t(), Yog.node_id(), keyword()) :: t()
Adds a person (user, actor, or role).
Options
:label(String.t/0) - Display label for the node.:description(String.t/0) - Longer description shown as tooltip.:technology(String.t/0) - Technology stack (e.g. 'Elixir, Phoenix').:shape(atom/0) - Shape override for DOT output.:fillcolor(String.t/0) - Background color override.:fontcolor(String.t/0) - Font color override.:style(String.t/0) - Style override.:penwidth- Border thickness override.:image(String.t/0) - Image/icon path or URL override.
Examples
iex> c4 = Choreo.C4.new() |> Choreo.C4.add_person(:customer, label: "Customer")
iex> :customer in Choreo.C4.nodes(c4)
true
iex> Map.get(c4.graph.nodes, :customer).node_type
:person
@spec add_relationship(t(), Yog.node_id(), Yog.node_id(), keyword()) :: t()
Adds a relationship (directed edge) between two C4 elements.
Options
:label(String.t/0) - Description of the relationship.:technology(String.t/0) - Technology used for the relationship (e.g. 'HTTPS', 'gRPC').
Examples
iex> c4 = Choreo.C4.new()
...> |> Choreo.C4.add_person(:customer)
...> |> Choreo.C4.add_software_system(:banking)
...> |> Choreo.C4.add_relationship(:customer, :banking, label: "Uses")
iex> Choreo.C4.edges(c4)
[{:customer, :banking, 1}]
@spec add_software_system(t(), Yog.node_id(), keyword()) :: t()
Adds a software system.
Options
:scope- Whether this system is in scope (:in) or external (:out) of the diagram being produced. The default value is:out.:label(String.t/0) - Display label for the node.:description(String.t/0) - Longer description shown as tooltip.:technology(String.t/0) - Technology stack (e.g. 'Elixir, Phoenix').:shape(atom/0) - Shape override for DOT output.:fillcolor(String.t/0) - Background color override.:fontcolor(String.t/0) - Font color override.:style(String.t/0) - Style override.:penwidth- Border thickness override.:image(String.t/0) - Image/icon path or URL override.
Examples
iex> c4 = Choreo.C4.new() |> Choreo.C4.add_software_system(:banking, label: "Banking", scope: :in)
iex> :banking in Choreo.C4.nodes(c4)
true
iex> Map.get(c4.graph.nodes, :banking).node_type
:software_system
iex> Map.get(c4.graph.nodes, :banking).scope
:in
@spec children(t(), Yog.node_id()) :: [Yog.node_id()]
Returns all children of a given parent node.
Examples
iex> c4 = Choreo.C4.new()
...> |> Choreo.C4.add_software_system(:banking)
...> |> Choreo.C4.add_container(:api, parent: :banking)
...> |> Choreo.C4.add_container(:web, parent: :banking)
iex> Enum.sort(Choreo.C4.children(c4, :banking))
[:api, :web]
Clears the explicit scope and removes all scope: :in tags.
Examples
iex> c4 = Choreo.C4.new() |> Choreo.C4.add_software_system(:banking)
iex> c4 = Choreo.C4.set_scope(c4, :banking)
iex> c4 = Choreo.C4.clear_scope(c4)
iex> c4.scope
nil
iex> c4 = Choreo.C4.new() |> Choreo.C4.add_software_system(:banking, scope: :in)
iex> c4 = Choreo.C4.clear_scope(c4)
iex> c4.scope
nil
iex> Map.get(c4.graph.nodes, :banking).scope
:out
@spec edges(t()) :: [{Yog.node_id(), Yog.node_id(), number()}]
Returns all edges as {from, to, weight} tuples.
Examples
iex> c4 = Choreo.C4.new()
...> |> Choreo.C4.add_person(:a)
...> |> Choreo.C4.add_software_system(:b)
...> |> Choreo.C4.add_relationship(:a, :b)
iex> Choreo.C4.edges(c4)
[{:a, :b, 1}]
@spec edges_with_meta(t()) :: [{Yog.node_id(), Yog.node_id(), number(), map()}]
Returns all edges with their metadata as {from, to, weight, meta} tuples.
Examples
iex> c4 = Choreo.C4.new()
...> |> Choreo.C4.add_person(:a)
...> |> Choreo.C4.add_software_system(:b)
...> |> Choreo.C4.add_relationship(:a, :b, label: "Uses")
iex> [{_, _, _, meta}] = Choreo.C4.edges_with_meta(c4)
iex> meta.label
"Uses"
Creates a new empty C4 model.
Examples
iex> c4 = Choreo.C4.new()
iex> Choreo.C4.nodes(c4)
[]
iex> Choreo.C4.edges(c4)
[]
iex> c4.scope
nil
@spec nodes(t()) :: [Yog.node_id()]
Returns all node IDs in the C4 model.
Examples
iex> c4 = Choreo.C4.new() |> Choreo.C4.add_person(:a) |> Choreo.C4.add_software_system(:b)
iex> Enum.sort(Choreo.C4.nodes(c4))
[:a, :b]
@spec nodes_of_type(t(), atom()) :: [Yog.node_id()]
Returns all nodes of a given C4 type.
Examples
iex> c4 = Choreo.C4.new()
...> |> Choreo.C4.add_person(:a)
...> |> Choreo.C4.add_person(:b)
...> |> Choreo.C4.add_software_system(:c)
iex> Enum.sort(Choreo.C4.nodes_of_type(c4, :person))
[:a, :b]
iex> Choreo.C4.nodes_of_type(c4, :software_system)
[:c]
@spec parent(t(), Yog.node_id()) :: Yog.node_id() | nil
Returns the parent ID of a node, or nil.
Examples
iex> c4 = Choreo.C4.new()
...> |> Choreo.C4.add_software_system(:banking)
...> |> Choreo.C4.add_container(:api, parent: :banking)
iex> Choreo.C4.parent(c4, :api)
:banking
iex> Choreo.C4.parent(c4, :banking)
nil
@spec scope(t()) :: Yog.node_id() | nil
Returns the scope node ID, or nil.
Examples
iex> c4 = Choreo.C4.new() |> Choreo.C4.add_software_system(:banking)
iex> c4 = Choreo.C4.set_scope(c4, :banking)
iex> Choreo.C4.scope(c4)
:banking
@spec set_scope(t(), Yog.node_id()) :: t()
Sets the in-scope node for drill-down diagrams.
When generating a Container diagram, the scope is typically the software system being expanded. For a Component diagram, the scope is the container being expanded.
Examples
iex> c4 = Choreo.C4.new() |> Choreo.C4.add_software_system(:banking, scope: :in)
iex> c4 = Choreo.C4.set_scope(c4, :banking)
iex> c4.scope
:banking
@spec theme( atom(), keyword() ) :: Choreo.Theme.t()
Returns a theme for Choreo.C4 diagrams.
Examples
iex> theme = Choreo.C4.theme(:default)
iex> theme.name
:c4_default
Renders the C4 model to DOT format.
Options
:theme—:default,:dark,:warm,:forest,:ocean, or aChoreo.Themestruct:syntax—:default(C4-style boxes) or:nested(clusters by parent)
Examples
iex> c4 = Choreo.C4.new() |> Choreo.C4.add_person(:a)
iex> dot = Choreo.C4.to_dot(c4)
iex> String.contains?(dot, "digraph")
true
@spec to_graph(t()) :: Yog.Multi.Graph.t()
Returns the raw Yog.Multi.Graph struct underpinning the model.
Examples
iex> c4 = Choreo.C4.new()
iex> graph = Choreo.C4.to_graph(c4)
iex> graph.kind
:directed
Renders the C4 model to Mermaid.js flowchart syntax.
Options
:theme—:default,:dark,:warm,:forest,:ocean, or aChoreo.Themestruct:direction—:lr(default),:td,:rl,:bt
Examples
iex> c4 = Choreo.C4.new() |> Choreo.C4.add_person(:a)
iex> mermaid = Choreo.C4.to_mermaid(c4)
iex> String.contains?(mermaid, "graph LR")
true