Skip to main content

C4 Model

· 7 min read

The C4 Model is a diagramming framework. Over the years, we've had many practitioners request C4 features in D2. There's even a community-maintained exporter from the C4 creator's tool, Structurizr, to D2.

The C4 Model is a loose framework. Unlike UML, which says these symbols always and must mean certain things, the C4 model is a set of diagramming concepts. It's language and tool agnostic, and these powerful concepts have proven to provide software projects with clean, mature architecture diagrams.

With the latest 0.7 release of D2, we filled the gaps in the language to have first-class support of these concepts:

  1. A new suspend keyword which, along with existing D2 features, lets you define one model and reuse them across different views.
  2. Markdown labels on shapes allow you to give richer textual descriptions with font hierarchy.
  3. A new c4-person shape that is more conducive to holding longform labels.
  4. A C4 theme for diagrams to be conveniently on-brand recognizable.
  5. A new d2-legend variable to easily make beautiful legends.

Whether you choose to use these new features specifically for C4 model diagrams is up to you. Much of this feature set was requested for other purposes. What's important are the good practices it enables, which can be applied broadly across all sorts of diagrams.

In this article, I'll demonstrate how you can make C4 diagrams in D2. If you'd like more detail on using a specific feature, you'll find dedicated sections in the docs.

One model, multiple views

Let's take a look at how the new suspend keyword can be used to slice and dice a model into a variety of views.

First we define a medium-sized diagram using the new C4 theme, c4-person shape, and markdown labels.

Personal Banking Customer

[person]

A customer of the bank, with personal bank accounts.

Internet Banking System

[Software System]

E-mail System

[Software System]

The internal Microsoft Exchange e-mail system.

Mainframe Banking System

[Software System]

Stores all of the core banking information about customers, accounts, transactions, etc.

Web Application

[Container: Java and Spring MVC]

Delivers the static content and the Internet banking single page application.

Single-Page Application

[Container: JavaScript and Angular]

Provides all of the Internet banking functionality to customers via their web browser.

Mobile App

[Container: Xamarin]

Provides a limited subset of the Internet banking functionality to customers via their mobile device.

API Application

[Container: Java and Spring MVC]

Provides Internet banking functionality via a JSON/HTTPS API.

Database

[Container: Oracle Database Schema]

Stores user registration information, hashed authentication credentials, access logs, etc.

Visits bigbank.com/ib using[HTTPS]Delivers to the customer's web browserViews account balances,and makes payments usingViews account balances,and makes payments usingMakes API calls to[JSON/HTTPS]Makes API calls to[JSON/HTTPS]Makes API calls to[XML/HTTPS]Sends e-mails toSends e-mail using Reads from and writes to[SQL/TCP]

Show only relationships to API Application

Let's say we want to create a view for the API team that only includes what's relevant for them. We'll use the same code as the above, but add 2 sections:

1. "Suspend" all models

Use globs to target everything declared so far and suspend them. These will be removed unless "unsuspended" later on. Suspended objects and connections are effectively the models.

2. Unsuspend models we use for our view

Now we use globs to declare which models we want to show up. We do this by "unsuspending" them.

Result

Internet Banking System

[Software System]

E-mail System

[Software System]

The internal Microsoft Exchange e-mail system.

Mainframe Banking System

[Software System]

Stores all of the core banking information about customers, accounts, transactions, etc.

Single-Page Application

[Container: JavaScript and Angular]

Provides all of the Internet banking functionality to customers via their web browser.

Mobile App

[Container: Xamarin]

Provides a limited subset of the Internet banking functionality to customers via their mobile device.

API Application

[Container: Java and Spring MVC]

Provides Internet banking functionality via a JSON/HTTPS API.

Makes API calls to[JSON/HTTPS]Makes API calls to[JSON/HTTPS]Makes API calls to[XML/HTTPS]Sends e-mail using

Runnable code:

Show high-level overview

Let's make a high level overview with the same concepts. The only change here is how we unsuspend things.

And since there are connections between inner shapes of Internet Banking System to other components, we'll make some connections that capture the general dependency.

Personal Banking Customer

[person]

A customer of the bank, with personal bank accounts.

Internet Banking System

[Software System]

E-mail System

[Software System]

The internal Microsoft Exchange e-mail system.

Mainframe Banking System

[Software System]

Stores all of the core banking information about customers, accounts, transactions, etc.

Sends e-mails toSend requestsAPI callsSend emails

Runnable code:

Tags and filters

Let's say your repository of models is large; maybe everyone in your cross-functional team is pushing their own models to this file.

To give an example of a small repository:

ATM Banking System

[Software System]

Allows customers to perform financial transactions via physical terminals.

Fraud Detection System

[Software System]

Monitors transactions for suspicious activity using ML algorithms.

Customer Service Portal

[Software System]

Tools for customer service representatives to assist clients.

Notification System

[Container: Java]

Sends alerts and notifications to customers via multiple channels.

Payment Gateway

[Container: Python & Django]

Processes payments and interfaces with external payment networks.

Backup System

[Container: Azure Cloud Storage]

Automated backup of critical banking data.

Administrative Portal

[Container: React]

Administrative interface for system maintenance and user management.

Security System

[Software System]

Manages authentication, authorization, and security policies.

Reporting Engine

[Container: Python & Pandas]

Generates reports and analytics for internal stakeholders.

Logging Service

[Container: ELK Stack]

Centralized logging for auditing and debugging purposes.

Compliance System

[Software System]

Ensures banking activities comply with regulations.

Customer Chatbot

[Container: Node.js & NLP]

AI-powered chat interface for customer support.

KYC Verification

[Software System]

Know Your Customer identity verification process.

Loan Processing System

[Container: Java EE]

Handles loan applications and approval workflows.

Investment Platform

[Software System]

Provides trading and investment capabilities to customers.

Account Management

[Container: Java & Spring Boot]

Core functions for creating and managing customer accounts.

Bill Payment Service

[Container: Node.js]

Allows customers to schedule and pay bills electronically.

Transaction Processor

[Container: Scala]

High-performance engine for processing financial transactions.

Data Warehouse

[Container: Snowflake]

Enterprise data repository for analytics and business intelligence.

Mobile Banking API

[Container: GraphQL & Node.js]

API gateway specifically optimized for mobile client applications.

To reuse these models for a specific domain, we can filter by their classes (tag in C4 vernacular). Assuming the models are in their own file, we can create something like a CustomerInterface.d2 file, which imports these models, suspends all, and unsuspends the ones that have the customer-facing class.

ATM Banking System

[Software System]

Allows customers to perform financial transactions via physical terminals.

Notification System

[Container: Java]

Sends alerts and notifications to customers via multiple channels.

Customer Chatbot

[Container: Node.js & NLP]

AI-powered chat interface for customer support.

KYC Verification

[Software System]

Know Your Customer identity verification process.

Investment Platform

[Software System]

Provides trading and investment capabilities to customers.

Bill Payment Service

[Container: Node.js]

Allows customers to schedule and pay bills electronically.

Mobile Banking API

[Container: GraphQL & Node.js]

API gateway specifically optimized for mobile client applications.

Now we can add some connections to create a diagram out of these pre-existing models.

ATM Banking System

[Software System]

Allows customers to perform financial transactions via physical terminals.

Notification System

[Container: Java]

Sends alerts and notifications to customers via multiple channels.

Customer Chatbot

[Container: Node.js & NLP]

AI-powered chat interface for customer support.

KYC Verification

[Software System]

Know Your Customer identity verification process.

Investment Platform

[Software System]

Provides trading and investment capabilities to customers.

Bill Payment Service

[Container: Node.js]

Allows customers to schedule and pay bills electronically.

Mobile Banking API

[Container: GraphQL & Node.js]

API gateway specifically optimized for mobile client applications.

Integration Sends alerts via Verifies identity Provides access Answers questions Allows paymentsAuthenticatesSends alerts

Legend

To create a legend for this diagram, we declare it as a variable under d2-legend.

Think of d2-legend as a mini diagram of its own, but with a special layout where every shape and connection is deconstructed into a table. If something has opacity 0, it is excluded in the table (this way we can have legends that only show connections, for example).

Notice the legend in the bottom right corner.

ATM Banking System

[Software System]

Allows customers to perform financial transactions via physical terminals.

Notification System

[Container: Java]

Sends alerts and notifications to customers via multiple channels.

Customer Chatbot

[Container: Node.js & NLP]

AI-powered chat interface for customer support.

KYC Verification

[Software System]

Know Your Customer identity verification process.

Investment Platform

[Software System]

Provides trading and investment capabilities to customers.

Bill Payment Service

[Container: Node.js]

Allows customers to schedule and pay bills electronically.

Mobile Banking API

[Container: GraphQL & Node.js]

API gateway specifically optimized for mobile client applications.

Integration Sends alerts via Verifies identity Provides access Answers questions Allows paymentsAuthenticatesSends alertsLegendBanking Alerting Authentication

Google Maps zooming

Lastly, a core concept of C4 diagrams is this notion of zooming in and out of different levels of abstraction. The 4 in C4 stands for the 4 levels of abstractions that it recommends. Let's take a look at how that can be achieved. For this example, we'll define two diagrams of code that represent a zoomed in view of the components.

Example code diagram for ATM Banking System

func StartServer(port interror func SetupRoutes(router *mux.Router) func AuthMiddleware(next http.Handler) http.Handlerfunc StartServer(port int) error func SetupRoutes(router *mux.Router) func AuthMiddleware(next http.Handler) http.Handlerfunc NewHandler(schema *graphql.Schema) *Handler func ServeHTTP(w http.ResponseWriter, r *http.Request) func ExecuteQuery(query string, variables map[string]interface{}) *Resultfunc NewHandler(schema *graphql.Schema) *Handler func ServeHTTP(w http.ResponseWriter, r *http.Request) func ExecuteQuery(query string, variables map[string]interface{}) *Resultfunc AccountResolver(p graphql.ResolveParams) (interface{}, error) func TransactionResolver(p graphql.ResolveParams) (interface{}, error) func UserResolver(p graphql.ResolveParams) (interface{}, error)func AccountResolver(p graphql.ResolveParams) (interface{}, error) func TransactionResolver(p graphql.ResolveParams) (interface{}, error) func UserResolver(p graphql.ResolveParams) (interface{}, error)func BuildSchema() (*graphql.Schema, error) func RegisterTypes(schema *graphql.Schema) func RegisterQueries(schema *graphql.Schema) func RegisterMutations(schema *graphql.Schema)func BuildSchema() (*graphql.Schema, error) func RegisterTypes(schema *graphql.Schema) func RegisterQueries(schema *graphql.Schema) func RegisterMutations(schema *graphql.Schema)func NewAccountClient(baseURL string*AccountClient func NewTransactionClient(baseURL string*TransactionClient func (c *AccountClient) GetAccount(id string) (*Account, error) func (c *TransactionClient) ListTransactions(accountID string) ([]*Transaction, error)func NewAccountClient(baseURL string) *AccountClient func NewTransactionClient(baseURL string) *TransactionClient func (c *AccountClient) GetAccount(id string) (*Account, error) func (c *TransactionClient) ListTransactions(accountID string) ([]*Transaction, error) uses calls usescalls references

Example code diagram for Notification System

func NewNotificationManager(config *Config) *NotificationManager func (m *NotificationManager) SendNotification(userID string, message *Message) error func (m *NotificationManager) SendBulkNotifications(userIDs []string, message *Message) []errorfunc NewNotificationManager(config *Config) *NotificationManager func (m *NotificationManager) SendNotification(userID string, message *Message) error func (m *NotificationManager) SendBulkNotifications(userIDs []string, message *Message) []errorfunc NewChannelRouter(channels []Channel) *ChannelRouter func (r *ChannelRouter) RouteNotification(userID string, message *Message) (Channel, error) func (r *ChannelRouter) GetUserPreferences(userID string) (*UserPreferences, error)func NewChannelRouter(channels []Channel) *ChannelRouter func (r *ChannelRouter) RouteNotification(userID string, message *Message) (Channel, error) func (r *ChannelRouter) GetUserPreferences(userID string) (*UserPreferences, error)func NewEmailChannel(smtpConfig *SMTPConfig) *EmailChannel func (c *EmailChannel) Send(userID string, message *Message) error func (c *EmailChannel) FormatMessage(message *Message) (stringerror)func NewEmailChannel(smtpConfig *SMTPConfig) *EmailChannel func (c *EmailChannel) Send(userID string, message *Message) error func (c *EmailChannel) FormatMessage(message *Message) (string, error)func NewSMSChannel(twilioConfig *TwilioConfig) *SMSChannel func (c *SMSChannel) Send(userID string, message *Message) error func (c *SMSChannel) FormatMessage(message *Message) (stringerror)func NewSMSChannel(twilioConfig *TwilioConfig) *SMSChannel func (c *SMSChannel) Send(userID string, message *Message) error func (c *SMSChannel) FormatMessage(message *Message) (string, error)func NewPushChannel(fcmConfig *FCMConfig) *PushChannel func (c *PushChannel) Send(userID string, message *Message) error func (c *PushChannel) FormatMessage(message *Message) (map[string]interface{}, error)func NewPushChannel(fcmConfig *FCMConfig) *PushChannel func (c *PushChannel) Send(userID string, message *Message) error func (c *PushChannel) FormatMessage(message *Message) (map[string]interface{}, error)func NewTemplateEngine(templateDir string*TemplateEngine func (e *TemplateEngine) RenderTemplate(templateName string, data map[string]interface{}) (stringerror) func (e *TemplateEngine) RegisterHelper(name string, helper interface{}) errorfunc NewTemplateEngine(templateDir string) *TemplateEngine func (e *TemplateEngine) RenderTemplate(templateName string, data map[string]interface{}) (string, error) func (e *TemplateEngine) RegisterHelper(name string, helper interface{}) error uses routes toroutes toroutes to rendersrendersrenders

Linking together

Now let's link these up using the layers feature.

All that's needed is to declare the 2 code diagrams as layers and add the link property to whichever shape we want to be able to zoom into them.

We can export this to a PDF format. If you download it and click on notification or banking, it'll take you to those respective zoomed-in code views.

To recap, you'd split it out into those 4 files and use imports for modularity:

  1. models.d2: where models are defined
  2. customer-components.d2: where you import models
  3. banking-code.d2: code diagram for banking system
  4. notification-code.d2: code diagram for notification system

You can easily flesh this out further by having files for the styling, for higher levels of abstraction (e.g. for context diagrams).