|
| 1 | +import CodeBlock from '@theme/CodeBlock'; |
| 2 | +import C4 from '@site/static/d2/c4.d2'; |
| 3 | +import C4View1 from '@site/static/d2/c4-view1.d2'; |
| 4 | +import C4View2 from '@site/static/d2/c4-view2.d2'; |
| 5 | +import C4Models from '@site/static/d2/c4-models.d2'; |
| 6 | +import C4Tags2 from '@site/static/d2/c4-tags2.d2'; |
| 7 | +import C4Tags3 from '@site/static/d2/c4-tags3.d2'; |
| 8 | +import C4Legend from '@site/static/d2/c4-legend.d2'; |
| 9 | +import C4Code1 from '@site/static/d2/c4-code1.d2'; |
| 10 | +import C4Code2 from '@site/static/d2/c4-code2.d2'; |
| 11 | +import C4All from '@site/static/bespoke-d2/c4-code.d2'; |
| 12 | + |
| 13 | +# C4 Model |
| 14 | + |
| 15 | +The [C4 Model](https://c4model.com/) is a diagramming framework. Over the years, we've had |
| 16 | +many practitioners request C4 features in D2. There's even a community-maintained |
| 17 | +[exporter](https://github.com/goto1134/structurizr-d2-exporter) from the C4 creator's |
| 18 | +tool, [Structurizr](https://docs.structurizr.com/cli/export#options), to D2. |
| 19 | + |
| 20 | +The C4 Model is a loose framework. Unlike UML, which says these symbols always and must |
| 21 | +mean certain things, the C4 model is a set of *diagramming concepts*. It's language and |
| 22 | +tool agnostic, and these powerful concepts have proven to provide software projects with |
| 23 | +clean, mature architecture diagrams. |
| 24 | + |
| 25 | +With the latest 0.7 release of D2, we filled the gaps in the language to have first-class |
| 26 | +support of these concepts: |
| 27 | + |
| 28 | +1. A new `suspend` keyword which, along with existing D2 features, lets you define one |
| 29 | + model and reuse them across different views. |
| 30 | +2. Markdown labels on shapes allow you to give richer textual descriptions with font |
| 31 | + hierarchy. |
| 32 | +3. A new `c4-person` shape that is more conducive to holding longform labels. |
| 33 | +4. A C4 theme for diagrams to be conveniently on-brand recognizable. |
| 34 | +5. A new `d2-legend` variable to easily make beautiful legends. |
| 35 | + |
| 36 | +Whether you choose to use these new features specifically for C4 model diagrams is up to |
| 37 | +you. Much of this feature set was requested for other purposes. What's important are the |
| 38 | +good practices it enables, which can be applied broadly across all sorts of diagrams. |
| 39 | + |
| 40 | +In this article, I'll demonstrate how you can make C4 diagrams in D2. If you'd like more |
| 41 | +detail on using a specific feature, you'll find dedicated sections in the |
| 42 | +[docs](https://d2lang.com). |
| 43 | + |
| 44 | +## One model, multiple views |
| 45 | + |
| 46 | +Let's take a look at how the new `suspend` keyword can be used to slice and dice a model |
| 47 | +into a variety of views. |
| 48 | + |
| 49 | +First we define a medium-sized diagram using the new C4 theme, `c4-person` shape, and |
| 50 | +markdown labels. |
| 51 | + |
| 52 | +<CodeBlock className="language-d2" expandeable={true}> |
| 53 | + {C4} |
| 54 | +</CodeBlock> |
| 55 | + |
| 56 | +<div className="embedSVG" dangerouslySetInnerHTML={{__html: require('@site/static/img/generated/c4.svg2')}}></div> |
| 57 | + |
| 58 | +### Show only relationships to API Application |
| 59 | + |
| 60 | +Let's say we want to create a view for the API team that only includes what's relevant for |
| 61 | +them. We'll use the same code as the above, but add 2 sections: |
| 62 | + |
| 63 | +#### 1. "Suspend" all models |
| 64 | + |
| 65 | +Use globs to target everything declared so far and suspend them. These will be removed |
| 66 | +unless "unsuspended" later on. Suspended objects and connections are effectively the |
| 67 | +models. |
| 68 | + |
| 69 | +```d2 |
| 70 | +# Suspend all shapes |
| 71 | +**: suspend |
| 72 | +# Suspend all connections |
| 73 | +(** -> **)[*]: suspend |
| 74 | +``` |
| 75 | + |
| 76 | +#### 2. Unsuspend models we use for our view |
| 77 | + |
| 78 | +Now we use globs to declare which models we want to show up. We do this by "unsuspending" |
| 79 | +them. |
| 80 | + |
| 81 | +```d2 |
| 82 | +# Show every connection with a source to API Application |
| 83 | +(** -> **)[*]: unsuspend { |
| 84 | + &src: internet_banking_system.api_app |
| 85 | +} |
| 86 | +# Show every connection with a destination to API Application |
| 87 | +(** -> **)[*]: unsuspend { |
| 88 | + &dst: internet_banking_system.api_app |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +#### Result |
| 93 | + |
| 94 | +<div className="embedSVG" dangerouslySetInnerHTML={{__html: require('@site/static/img/generated/c4-view1.svg2')}}></div> |
| 95 | + |
| 96 | +Runnable code: |
| 97 | +<CodeBlock className="language-d2" expandeable={true}> |
| 98 | + {C4View1} |
| 99 | +</CodeBlock> |
| 100 | + |
| 101 | +### Show high-level overview |
| 102 | + |
| 103 | +Let's make a high level overview with the same concepts. The only change here is how we |
| 104 | +unsuspend things. |
| 105 | + |
| 106 | +```d2 |
| 107 | +# Only show root level shapes |
| 108 | +**: unsuspend { |
| 109 | + &level: 0 |
| 110 | +} |
| 111 | +# Only show connections between root level things |
| 112 | +(** -> **)[*]: unsuspend { |
| 113 | + &src.level: 0 |
| 114 | + &dst.level: 0 |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +And since there are connections between inner shapes of Internet Banking System to other |
| 119 | +components, we'll make some connections that capture the general dependency. |
| 120 | + |
| 121 | +```d2 |
| 122 | +customer -> internet_banking_system: Send requests |
| 123 | +internet_banking_system -> mainframe: API calls |
| 124 | +internet_banking_system -> email_system: Send emails |
| 125 | +``` |
| 126 | + |
| 127 | +<div className="embedSVG" dangerouslySetInnerHTML={{__html: require('@site/static/img/generated/c4-view2.svg2')}}></div> |
| 128 | + |
| 129 | +Runnable code: |
| 130 | +<CodeBlock className="language-d2" expandeable={true}> |
| 131 | + {C4View2} |
| 132 | +</CodeBlock> |
| 133 | + |
| 134 | +## Tags and filters |
| 135 | + |
| 136 | +Let's say your repository of models is large; maybe everyone in your cross-functional team |
| 137 | +is pushing their own models to this file. |
| 138 | + |
| 139 | +To give an example of a small repository: |
| 140 | + |
| 141 | +<div className="embedSVG" dangerouslySetInnerHTML={{__html: require('@site/static/img/generated/c4-models.svg2')}}></div> |
| 142 | + |
| 143 | +<CodeBlock className="language-d2" expandeable={true}> |
| 144 | + {C4Models} |
| 145 | +</CodeBlock> |
| 146 | + |
| 147 | +To reuse these models for a specific domain, we can filter by their classes (tag in C4 |
| 148 | +vernacular). Assuming the models are in their own file, we can create something like a |
| 149 | +CustomerInterface.d2 file, which imports these models, suspends all, and unsuspends the |
| 150 | +ones that have the `customer-facing` class. |
| 151 | + |
| 152 | +<CodeBlock className="language-d2-incomplete"> |
| 153 | + {C4Tags2} |
| 154 | +</CodeBlock> |
| 155 | + |
| 156 | +<div className="embedSVG" dangerouslySetInnerHTML={{__html: require('@site/static/img/generated/c4-tags2.svg2')}}></div> |
| 157 | + |
| 158 | +Now we can add some connections to create a diagram out of these pre-existing models. |
| 159 | + |
| 160 | +<CodeBlock className="language-d2-incomplete" expandeable={true}> |
| 161 | + {C4Tags3} |
| 162 | +</CodeBlock> |
| 163 | + |
| 164 | +<div className="embedSVG" dangerouslySetInnerHTML={{__html: require('@site/static/img/generated/c4-tags3.svg2')}}></div> |
| 165 | + |
| 166 | +## Legend |
| 167 | + |
| 168 | +To create a legend for this diagram, we declare it as a [variable](/tour/vars) under |
| 169 | +`d2-legend`. |
| 170 | + |
| 171 | +Think of `d2-legend` as a mini diagram of its own, but with a special layout where every |
| 172 | +shape and connection is deconstructed into a table. If something has opacity 0, it is |
| 173 | +excluded in the table (this way we can have legends that only show connections, for |
| 174 | +example). |
| 175 | + |
| 176 | +```d2 |
| 177 | +vars: { |
| 178 | + d2-legend: { |
| 179 | + banking: Banking { |
| 180 | + style.fill: "#FFE4E1" |
| 181 | + } |
| 182 | + banking -> x: Alerting { |
| 183 | + style.stroke-dash: 5 |
| 184 | + style.stroke: "#2E8B57" |
| 185 | + } |
| 186 | + banking -> y: Authentication { |
| 187 | + style.stroke-width: 2 |
| 188 | + style.stroke: "#800080" |
| 189 | + } |
| 190 | + # Remove unneeded objects |
| 191 | + x.style.opacity: 0 |
| 192 | + y.style.opacity: 0 |
| 193 | + } |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +Notice the legend in the bottom right corner. |
| 198 | + |
| 199 | +<div className="embedSVG" dangerouslySetInnerHTML={{__html: require('@site/static/img/generated/c4-legend.svg2')}}></div> |
| 200 | + |
| 201 | +## Google Maps zooming |
| 202 | + |
| 203 | +Lastly, a core concept of C4 diagrams is this notion of zooming in and out of different |
| 204 | +levels of abstraction. The 4 in C4 stands for the 4 levels of abstractions that it |
| 205 | +recommends. Let's take a look at how that can be achieved. For this example, we'll define |
| 206 | +two diagrams of code that represent a zoomed in view of the components. |
| 207 | + |
| 208 | +**Example code diagram for ATM Banking System** |
| 209 | + |
| 210 | +<CodeBlock className="language-d2-incomplete" expandeable={true}> |
| 211 | + {C4Code1} |
| 212 | +</CodeBlock> |
| 213 | + |
| 214 | +<div className="embedSVG" dangerouslySetInnerHTML={{__html: require('@site/static/img/generated/c4-code1.svg2')}}></div> |
| 215 | + |
| 216 | +**Example code diagram for Notification System** |
| 217 | + |
| 218 | +<CodeBlock className="language-d2-incomplete" expandeable={true}> |
| 219 | + {C4Code2} |
| 220 | +</CodeBlock> |
| 221 | + |
| 222 | +<div className="embedSVG" dangerouslySetInnerHTML={{__html: require('@site/static/img/generated/c4-code2.svg2')}}></div> |
| 223 | + |
| 224 | +### Linking together |
| 225 | + |
| 226 | +Now let's link these up using the [layers](/tour/composition) feature. |
| 227 | + |
| 228 | +All that's needed is to declare the 2 code diagrams as layers and add the `link` property |
| 229 | +to whichever shape we want to be able to zoom into them. |
| 230 | + |
| 231 | +```d2 |
| 232 | +...@c4-models.d2 |
| 233 | +**: suspend |
| 234 | +**: unsuspend { |
| 235 | + &class: customer-facing |
| 236 | +} |
| 237 | +
|
| 238 | +notification_system.link: layers.notification |
| 239 | +mobile_banking_api.link: layers.banking |
| 240 | +
|
| 241 | +# Connections for component diagram |
| 242 | +# ... |
| 243 | +
|
| 244 | +layers: { |
| 245 | + banking: @banking-code |
| 246 | + notification: @notification-code |
| 247 | +} |
| 248 | +``` |
| 249 | + |
| 250 | +We can export this to a PDF format. If you download it and click on notification or |
| 251 | +banking, it'll take you to those respective zoomed-in code views. |
| 252 | + |
| 253 | +<embed src={require('@site/static/img/generated/c4.pdf').default} width="100%" height="800" |
| 254 | + type="application/pdf" /> |
| 255 | + |
| 256 | +To recap, you'd split it out into those 4 files and use imports for modularity: |
| 257 | +1. `models.d2`: where models are defined |
| 258 | +2. `customer-components.d2`: where you import models |
| 259 | +3. `banking-code.d2`: code diagram for banking system |
| 260 | +3. `notification-code.d2`: code diagram for notification system |
| 261 | + |
| 262 | +You can easily flesh this out further by having files for the styling, for higher levels |
| 263 | +of abstraction (e.g. for context diagrams). |
0 commit comments