Choreo (Choreo v0.7.0)

Copy Markdown View Source

A domain-specific layer on top of Yog for modeling system architectures.

Choreo lets you define infrastructure diagrams (databases, caches, services, networks, etc.) as graphs and render them to DOT format with system-diagram theming. You can also run graph algorithms such as MST and topological sort to analyze your architecture.

When to use

Use Choreo when you need to document, communicate, or analyze the infrastructure of a system — whether for architecture review, onboarding docs, cost optimisation, or resilience planning.

Quick Start

system =
  Choreo.new()
  |> Choreo.add_database(:db, name: "Postgres", kind: :postgres)
  |> Choreo.add_cache(:cache, name: "Redis", kind: :redis)
  |> Choreo.add_service(:api, name: "API Gateway")
  |> Choreo.connect(:api, :cache, cost: 5)
  |> Choreo.connect(:api, :db, cost: 10)

dot = Choreo.to_dot(system)

Algorithms

# Minimum spanning tree for cost optimisation
{:ok, mst} = Choreo.Analysis.mst(system)

# Topological sort for data-flow ordering
{:ok, order} = Choreo.Analysis.topological_sort(system)

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.6, ranksep=1.2]; node [shape=ellipse, style=filled, fillcolor="lightblue", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; cache [label="Redis", style="filled", fontcolor="white", fillcolor="#f59e0b", shape="octagon"]; api [label="API Gateway", style="filled", fontcolor="white", fillcolor="#10b981", shape="box3d"]; db [label="Postgres", style="filled", fontcolor="white", fillcolor="#3b82f6", shape="cylinder"]; api -> cache [label="5", penwidth="1.0", color="#64748b"]; api -> db [label="10", penwidth="1.0", color="#64748b", headport="n"]; }

Summary

Functions

Adds a cache node to the system.

Defines a cluster (subgraph) for grouping nodes visually.

Adds a database node to the system.

Adds a data-flow edge between two nodes.

Adds a load-balancer node to the system.

Adds a network / infrastructure boundary node.

Adds a generic node to the system.

Adds a queue / messaging node to the system.

Adds a service / application node to the system.

Adds a storage / blob node to the system.

Adds a user / external actor node to the system.

Returns all cluster definitions in the system.

Connects two nodes with a weighted edge.

Returns all edges in the system as a list of {from, to, cost} tuples.

Returns all edges with their metadata as {from, to, cost, meta} tuples.

Embeds another diagram inside a cluster of the current system.

Creates a new empty system diagram.

Returns all nodes in the system as a map of id => data.

Renders the system diagram to DOT format.

Returns the raw Yog.Multi.Graph struct underpinning the system.

Collapses parallel edges into a simple Yog.Graph for algorithm analysis.

Types

t()

@type t() :: %Choreo{
  clusters: %{required(String.t()) => map()},
  edge_meta: %{optional(Yog.Multi.Graph.edge_id()) => map()},
  graph: Yog.Multi.Graph.t()
}

Functions

add_cache(system, id, opts \\ [])

@spec add_cache(t(), Yog.node_id(), keyword()) :: t()

Adds a cache node to the system.

Options

  • :kind (atom/0) - Specific kind of node (e.g. :postgres, :redis).

  • :name (String.t/0) - Display name (defaults to the node id).

  • :description (String.t/0) - Tooltip / annotation text.

  • :cluster (String.t/0) - Cluster name for visual grouping.

  • :shape (atom/0) - Shape override (e.g. :box, :cylinder).

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override (e.g. "filled").

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> system = Choreo.new() |> Choreo.add_cache(:cache, kind: :redis)
iex> Choreo.nodes(system)[:cache].type
:cache

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.6, ranksep=1.2]; node [shape=ellipse, style=filled, fillcolor="lightblue", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; cache [label="Redis", style="filled", fontcolor="white", fillcolor="#f59e0b", shape="octagon"]; }

add_cluster(system, name, opts \\ [])

@spec add_cluster(t(), String.t(), keyword()) :: t()

Defines a cluster (subgraph) for grouping nodes visually.

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 (e.g. "filled", "rounded").

  • :fillcolor (String.t/0) - Background color override.

  • :color (String.t/0) - Border color override.

Examples

iex> system = Choreo.new() |> Choreo.add_cluster("vpc", label: "VPC")
iex> Map.keys(Choreo.clusters(system))
["cluster_vpc"]

add_database(system, id, opts \\ [])

@spec add_database(t(), Yog.node_id(), keyword()) :: t()

Adds a database node to the system.

Options

  • :kind (atom/0) - Specific kind of node (e.g. :postgres, :redis).

  • :name (String.t/0) - Display name (defaults to the node id).

  • :description (String.t/0) - Tooltip / annotation text.

  • :cluster (String.t/0) - Cluster name for visual grouping.

  • :shape (atom/0) - Shape override (e.g. :box, :cylinder).

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override (e.g. "filled").

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> system = Choreo.new() |> Choreo.add_database(:db, kind: :postgres)
iex> Choreo.nodes(system)[:db].type
:database
iex> Choreo.nodes(system)[:db].kind
:postgres
iex> Choreo.nodes(system)[:db].name
"db"

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.6, ranksep=1.2]; node [shape=ellipse, style=filled, fillcolor="lightblue", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; db [label="db", style="filled", fontcolor="white", fillcolor="#3b82f6", shape="cylinder"]; }

add_dataflow(system, from, to, opts \\ [])

@spec add_dataflow(t(), Yog.node_id(), Yog.node_id(), keyword()) :: t()

Adds a data-flow edge between two nodes.

This is semantically identical to connect/4 but renders with a different default style (dashed, data-flow colour).

Examples

iex> system = Choreo.new() |> Choreo.add_service(:a) |> Choreo.add_service(:b) |> Choreo.add_dataflow(:a, :b)
iex> [{:a, :b, 1, meta}] = Choreo.edges_with_meta(system)
iex> meta.type
:dataflow

add_load_balancer(system, id, opts \\ [])

@spec add_load_balancer(t(), Yog.node_id(), keyword()) :: t()

Adds a load-balancer node to the system.

Options

  • :kind (atom/0) - Specific kind of node (e.g. :postgres, :redis).

  • :name (String.t/0) - Display name (defaults to the node id).

  • :description (String.t/0) - Tooltip / annotation text.

  • :cluster (String.t/0) - Cluster name for visual grouping.

  • :shape (atom/0) - Shape override (e.g. :box, :cylinder).

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override (e.g. "filled").

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> system = Choreo.new() |> Choreo.add_load_balancer(:lb)
iex> Choreo.nodes(system)[:lb].type
:load_balancer

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.6, ranksep=1.2]; node [shape=ellipse, style=filled, fillcolor="lightblue", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; lb [label="lb", style="filled", fontcolor="white", fillcolor="#8b5cf6", shape="hexagon"]; }

add_network(system, id, opts \\ [])

@spec add_network(t(), Yog.node_id(), keyword()) :: t()

Adds a network / infrastructure boundary node.

Options

  • :kind (atom/0) - Specific kind of node (e.g. :postgres, :redis).

  • :name (String.t/0) - Display name (defaults to the node id).

  • :description (String.t/0) - Tooltip / annotation text.

  • :cluster (String.t/0) - Cluster name for visual grouping.

  • :shape (atom/0) - Shape override (e.g. :box, :cylinder).

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override (e.g. "filled").

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> system = Choreo.new() |> Choreo.add_network(:vpc)
iex> Choreo.nodes(system)[:vpc].type
:network

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.6, ranksep=1.2]; node [shape=ellipse, style=filled, fillcolor="lightblue", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; cloudflare [label="cloudflare", style="filled", fontcolor="white", fillcolor="#6366f1", shape="cloud"]; }

add_node(system, id, opts \\ [])

@spec add_node(t(), Yog.node_id(), keyword()) :: t()

Adds a generic node to the system.

Use this when none of the typed builders fit.

Examples

iex> system = Choreo.new() |> Choreo.add_node(:misc, name: "Custom")
iex> Choreo.nodes(system)[:misc].type
:generic
iex> Choreo.nodes(system)[:misc].name
"Custom"

add_queue(system, id, opts \\ [])

@spec add_queue(t(), Yog.node_id(), keyword()) :: t()

Adds a queue / messaging node to the system.

Options

  • :kind (atom/0) - Specific kind of node (e.g. :postgres, :redis).

  • :name (String.t/0) - Display name (defaults to the node id).

  • :description (String.t/0) - Tooltip / annotation text.

  • :cluster (String.t/0) - Cluster name for visual grouping.

  • :shape (atom/0) - Shape override (e.g. :box, :cylinder).

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override (e.g. "filled").

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> system = Choreo.new() |> Choreo.add_queue(:q, kind: :kafka)
iex> Choreo.nodes(system)[:q].type
:queue

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.6, ranksep=1.2]; node [shape=ellipse, style=filled, fillcolor="lightblue", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; queue [label="queue", style="filled", fontcolor="white", fillcolor="#ec4899", shape="component"]; }

add_service(system, id, opts \\ [])

@spec add_service(t(), Yog.node_id(), keyword()) :: t()

Adds a service / application node to the system.

Options

  • :kind (atom/0) - Specific kind of node (e.g. :postgres, :redis).

  • :name (String.t/0) - Display name (defaults to the node id).

  • :description (String.t/0) - Tooltip / annotation text.

  • :cluster (String.t/0) - Cluster name for visual grouping.

  • :shape (atom/0) - Shape override (e.g. :box, :cylinder).

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override (e.g. "filled").

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> system = Choreo.new() |> Choreo.add_service(:api, name: "Gateway")
iex> Choreo.nodes(system)[:api].type
:service
iex> Choreo.nodes(system)[:api].name
"Gateway"

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.6, ranksep=1.2]; node [shape=ellipse, style=filled, fillcolor="lightblue", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; auth [label="auth", style="filled", fontcolor="white", fillcolor="#10b981", shape="box3d"]; }

add_storage(system, id, opts \\ [])

@spec add_storage(t(), Yog.node_id(), keyword()) :: t()

Adds a storage / blob node to the system.

Options

  • :kind (atom/0) - Specific kind of node (e.g. :postgres, :redis).

  • :name (String.t/0) - Display name (defaults to the node id).

  • :description (String.t/0) - Tooltip / annotation text.

  • :cluster (String.t/0) - Cluster name for visual grouping.

  • :shape (atom/0) - Shape override (e.g. :box, :cylinder).

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override (e.g. "filled").

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> system = Choreo.new() |> Choreo.add_storage(:s3)
iex> Choreo.nodes(system)[:s3].type
:storage

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.6, ranksep=1.2]; node [shape=ellipse, style=filled, fillcolor="lightblue", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; storage [label="storage", style="filled", fontcolor="white", fillcolor="#14b8a6", shape="tab"]; }

add_user(system, id, opts \\ [])

@spec add_user(t(), Yog.node_id(), keyword()) :: t()

Adds a user / external actor node to the system.

Options

  • :kind (atom/0) - Specific kind of node (e.g. :postgres, :redis).

  • :name (String.t/0) - Display name (defaults to the node id).

  • :description (String.t/0) - Tooltip / annotation text.

  • :cluster (String.t/0) - Cluster name for visual grouping.

  • :shape (atom/0) - Shape override (e.g. :box, :cylinder).

  • :fillcolor (String.t/0) - Background color override.

  • :fontcolor (String.t/0) - Font color override.

  • :style (String.t/0) - Style override (e.g. "filled").

  • :penwidth - Border thickness override.

  • :image (String.t/0) - Image/icon path or URL override.

Examples

iex> system = Choreo.new() |> Choreo.add_user(:client)
iex> Choreo.nodes(system)[:client].type
:user

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.6, ranksep=1.2]; node [shape=ellipse, style=filled, fillcolor="lightblue", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; user [label="user", style="filled", fontcolor="white", fillcolor="#ef4444", shape="doublecircle"]; }

clusters(choreo)

@spec clusters(t()) :: %{required(String.t()) => map()}

Returns all cluster definitions in the system.

Examples

iex> system = Choreo.new() |> Choreo.add_cluster("vpc", label: "VPC")
iex> Choreo.clusters(system)
%{"cluster_vpc" => %{label: "VPC"}}

connect(system, from, to, opts \\ [])

@spec connect(t(), Yog.node_id(), Yog.node_id(), keyword()) :: t()

Connects two nodes with a weighted edge.

Options

  • :cost - Numeric weight used by MST and algorithms. The default value is 1.

  • :label (String.t/0) - Display label for the edge.

  • :protocol - Communication protocol.

  • :type (atom/0) - Internal semantic type.

  • :headport (String.t/0) - Graphviz headport override.

  • :tailport (String.t/0) - Graphviz tailport override.

Examples

iex> system = Choreo.new() |> Choreo.add_service(:a) |> Choreo.add_service(:b) |> Choreo.connect(:a, :b, cost: 5)
iex> Choreo.edges(system)
[{:a, :b, 5}]

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.6, ranksep=1.2]; node [shape=ellipse, style=filled, fillcolor="lightblue", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [color="#64748b", style=solid, fontname="Helvetica", fontsize=10, penwidth=1.0]; a [label="a", style="filled", fontcolor="white", fillcolor="#10b981", shape="box3d"]; b [label="b", style="filled", fontcolor="white", fillcolor="#10b981", shape="box3d"]; a -> b [label="5", penwidth="1.0", color="#64748b"]; }

edges(choreo)

@spec edges(t()) :: [{Yog.node_id(), Yog.node_id(), number()}]

Returns all edges in the system as a list of {from, to, cost} tuples.

Examples

iex> system = Choreo.new() |> Choreo.add_service(:a) |> Choreo.add_service(:b) |> Choreo.connect(:a, :b)
iex> Choreo.edges(system)
[{:a, :b, 1}]

edges_with_meta(choreo)

@spec edges_with_meta(t()) :: [{Yog.node_id(), Yog.node_id(), number(), map()}]

Returns all edges with their metadata as {from, to, cost, meta} tuples.

Examples

iex> system = Choreo.new() |> Choreo.add_service(:a) |> Choreo.add_service(:b) |> Choreo.connect(:a, :b, protocol: :http)
iex> [{:a, :b, 1, meta}] = Choreo.edges_with_meta(system)
iex> meta.protocol
:http

embed(system, child_diagram, cluster_name, opts \\ [])

@spec embed(t(), struct(), String.t(), keyword()) :: t()

Embeds another diagram inside a cluster of the current system.

Options

  • :prefix - Prefix to prevent ID collisions. The default value is "sub_".

Examples

iex> system = Choreo.new() |> Choreo.add_cluster("vpc")
iex> flow = Choreo.Dataflow.new() |> Choreo.Dataflow.add_source(:in)
iex> system = Choreo.embed(system, flow, "vpc", prefix: "flow_")
iex> Map.has_key?(Choreo.nodes(system), :flow_in)
true

new(opts \\ [])

@spec new(keyword()) :: t()

Creates a new empty system diagram.

Options

  • :directed (boolean/0) - Whether the underlying graph is directed. The default value is true.

Examples

iex> system = Choreo.new()
iex> map_size(system.edge_meta)
0
iex> map_size(system.clusters)
0
iex> system = Choreo.new(directed: false)
iex> system.graph.kind
:undirected

nodes(choreo)

@spec nodes(t()) :: %{required(Yog.node_id()) => map()}

Returns all nodes in the system as a map of id => data.

Examples

iex> system = Choreo.new() |> Choreo.add_service(:api)
iex> Map.keys(Choreo.nodes(system))
[:api]

to_dot(diagram, opts \\ [])

@spec to_dot(
  any(),
  keyword()
) :: String.t()

Renders the system diagram to DOT format.

Options

Examples

iex> system = Choreo.new() |> Choreo.add_service(:api)
iex> dot = Choreo.to_dot(system)
iex> String.contains?(dot, "digraph")
true
iex> String.contains?(dot, "api")
true

to_graph(choreo)

@spec to_graph(t()) :: Yog.Multi.Graph.t()

Returns the raw Yog.Multi.Graph struct underpinning the system.

Useful when you want to drop down to the raw Yog Multi API.

Examples

iex> system = Choreo.new()
iex> Choreo.to_graph(system) == system.graph
true

to_simple_graph(choreo, opts \\ [])

@spec to_simple_graph(
  t(),
  keyword()
) :: Yog.Graph.t()

Collapses parallel edges into a simple Yog.Graph for algorithm analysis.

Options

  • :combine - function to combine weights of parallel edges (default: &min/2)

Examples

iex> system = Choreo.new() |> Choreo.add_service(:a) |> Choreo.add_service(:b)
...> |> Choreo.connect(:a, :b, cost: 10) |> Choreo.connect(:a, :b, cost: 5)
iex> simple = Choreo.to_simple_graph(system)
iex> Yog.all_edges(simple)
[{:a, :b, 5}]