diff --git a/diagrams/c4/__init__.py b/diagrams/c4/__init__.py
new file mode 100644
index 00000000..62b31717
--- /dev/null
+++ b/diagrams/c4/__init__.py
@@ -0,0 +1,113 @@
+"""
+A set of nodes and edges to visualize software architecture using the C4 model.
+"""
+import html
+import textwrap
+from diagrams import Cluster, Node, Edge
+
+
+def _format_node_label(name, type, description):
+ """Create a graphviz label string for a C4 node"""
+ title = f'{html.escape(name)}
'
+ subtitle = f'[{html.escape(type)}]
' if type else ""
+ text = f'
{_format_description(description)}' if description else ""
+ return f"<{title}{subtitle}{text}>"
+
+
+def _format_description(description):
+ """
+ Formats the description string so it fits into the C4 nodes.
+
+ It line-breaks the description so it fits onto exactly three lines. If there are more
+ than three lines, all further lines are discarded and "..." inserted on the last line to
+ indicate that it was shortened. This will also html-escape the description so it can
+ safely be included in a HTML label.
+ """
+ wrapper = textwrap.TextWrapper(width=40, max_lines=3)
+ lines = [html.escape(line) for line in wrapper.wrap(description)]
+ lines += [""] * (3 - len(lines)) # fill up with empty lines so it is always three
+ return "
".join(lines)
+
+
+def _format_edge_label(description):
+ """Create a graphviz label string for a C4 edge"""
+ wrapper = textwrap.TextWrapper(width=24, max_lines=3)
+ lines = [html.escape(line) for line in wrapper.wrap(description)]
+ text = "
".join(lines)
+ return f'<{text}>'
+
+
+def Container(name, type, description, **kwargs):
+ node_attributes = {
+ "label": _format_node_label(name, type, description),
+ "shape": "record",
+ "width": "2.6",
+ "height": "1.6",
+ "fixedsize": "true",
+ "style": "filled",
+ "fillcolor": "dodgerblue3",
+ "fontcolor": "white",
+ }
+ node_attributes.update(kwargs)
+ return Node(**node_attributes)
+
+
+def Database(name, type, description, **kwargs):
+ node_attributes = {
+ "label": _format_node_label(name, type, description),
+ "shape": "cylinder",
+ "width": "2.6",
+ "height": "1.6",
+ "fixedsize": "true",
+ "style": "filled",
+ "fillcolor": "dodgerblue3",
+ "fontcolor": "white",
+ }
+ node_attributes.update(kwargs)
+ return Node(**node_attributes)
+
+
+def System(name, description="", external=False, **kwargs):
+ type = "External System" if external else "System"
+ node_attributes = {
+ "label": _format_node_label(name, type, description),
+ "shape": "record",
+ "width": "2.6",
+ "height": "1.6",
+ "fixedsize": "true",
+ "style": "filled",
+ "fillcolor": "gray60" if external else "dodgerblue4",
+ "fontcolor": "white",
+ }
+ # collapse system boxes to a smaller form if they don't have a description
+ if not description:
+ node_attributes.update({"width": "2", "height": "1"})
+ node_attributes.update(kwargs)
+ return Node(**node_attributes)
+
+
+def Person(name, description, **kwargs):
+ node_attributes = {
+ "label": _format_node_label(name, "", description),
+ "shape": "record",
+ "width": "2.6",
+ "height": "1.6",
+ "fixedsize": "true",
+ "style": "rounded,filled",
+ "fillcolor": "dodgerblue4",
+ "fontcolor": "white",
+ }
+ node_attributes.update(kwargs)
+ return Node(**node_attributes)
+
+
+def SystemBoundary(name, **kwargs):
+ graph_attributes = {"label": html.escape(name), "bgcolor": "white", "margin": "16", "style": "dashed"}
+ graph_attributes.update(kwargs)
+ return Cluster(name, graph_attr=graph_attributes)
+
+
+def Dependency(label, **kwargs):
+ edge_attribtues = {"label": _format_edge_label(label), "style": "dashed", "color": "gray60"}
+ edge_attribtues.update(kwargs)
+ return Edge(**edge_attribtues)
diff --git a/tests/test_c4.py b/tests/test_c4.py
new file mode 100644
index 00000000..fa8d799f
--- /dev/null
+++ b/tests/test_c4.py
@@ -0,0 +1,50 @@
+import os
+import random
+import string
+import unittest
+
+from diagrams import Diagram
+from diagrams import setcluster, setdiagram
+from diagrams.c4 import Person, Container, Database, System, SystemBoundary, Dependency
+
+
+class C4Test(unittest.TestCase):
+ def setUp(self):
+ self.name = "diagram-" + "".join([random.choice(string.hexdigits) for n in range(7)])
+
+ def tearDown(self):
+ setdiagram(None)
+ setcluster(None)
+ try:
+ os.remove(self.name + ".png")
+ except FileNotFoundError:
+ pass
+
+ def test_nodes(self):
+ with Diagram(name=self.name, show=False):
+ person = Person("person", "A person.")
+ container = Container("container", "Java application", "The application.")
+ database = Database("database", "Oracle database", "Stores information.")
+
+ person >> container >> database
+
+ def test_systems(self):
+ with Diagram(name=self.name, show=False):
+ system = System("system", "The internal system.")
+ system_without_description = System("unknown")
+ external_system = System("external", "The external system.", external=True)
+
+ system >> system_without_description >> external_system
+
+ def test_edges(self):
+ with Diagram(name=self.name, show=False):
+ c1 = Container("container1", "type", "description")
+ c2 = Container("container2", "type", "description")
+
+ c1 >> Dependency("depends on") >> c2
+ c1 << Dependency("is dependend on") << c2
+
+ def test_cluster(self):
+ with Diagram(name=self.name, show=False):
+ with SystemBoundary("System"):
+ Container("container", "type", "description")