ποΈ Architecture as Code in Practice
Enhancing your architecture documentation with ADRs and C4 diagrams

This article builds on the foundation from my previous 'Architecture as Code' articles:
In my previous articles, I showed how keeping architecture diagrams as plain text (Markdown) in your repository creates a foundation for AI-readable documentation, and how specs help AI understand your design intent.
Model and sequence diagrams are commonly included in architecture documentation and can be written directly in Markdown using embedded Mermaid diagrams. But diagrams alone describe what the architecture isβnot why those choices were made.
Diagrams alone aren't enough for production systems. You also need:
Why decisions were made (the context and tradeβoffs)
Multiple views (from system context down to implementation)
A clear, scalable structure for your architecture documentation
This is where the approach in my previous articles alone isn't enough.
π― TL;DR: The Complete Structure
If you're just browsing online and want the essential takeaway, here it is:
architecture/
βββ adr/ # Why decisions were made
β βββ 0001-use-uuid-primary-keys.md
β βββ 0002-microservices-architecture.md
β βββ 0003-event-driven-communication.md
βββ c4-views/ # What the system looks like (4 zoom levels)
β βββ system-context.md
β βββ container.md
β βββ order-service-component.md
β βββ payment-service-component.md
βββ models/ # How entities relate and workflows
βββ domain/
β βββ customer.md
β βββ order.md
β βββ payment.md
βββ flows/
βββ create-order.md
βββ payment-processing.md
The three pillars: Three pillars: ADRs (why), C4 diagrams (what), and domain models (how). Keep everything as plain text, version-controlled, and AI-readable so architecture stays discoverable and actionable.
π Beyond Diagrams: Adding Context and Decisions
Diagrams (model, sequence, flow) explain what the system looks like and how it behaves, but they don't record the reasons behind design choices.
For production-grade documentation you need three things:
ADRs (templates) β record the context, alternatives, and rationale (why)
C4 diagrams (c4model.com) β show structure at multiple zoom levels (what)
A clear, scalable structure β where to find artifacts and how they relate (how/where)
Next I'll show how to organize these artifacts in your repo.
π Architecture Decision Records: Document the Why
Six months from now, someone will ask: "Why did we choose UUIDs instead of autoβincrement IDs?" If the answer only lives in Slack or in a person's head, the context is gone. ADRs fix that.
An ADR record:
The decision
Status and who approved it
The context and constraints at the time
Options that were considered
The chosen rationale
Consequences and tradeβoffs
The MADR Format
We use MADR (Markdown Architectural Decision Records) because it's lightweight, standardized, and plain textβeasy to review, search, and version with git.
Below is a simplified ADR example to illustrate the format:
# Use UUIDs for Primary Keys
* Status: accepted
* Date: 2026-01-12
* Deciders: Architecture Team
Technical Story: Choose a primary-key strategy for all database tables in our distributed e-commerce
platform.
## Context and Problem Statement
Our system is a distributed, service-oriented platform with requirements that include:
- Horizontal scaling of services
- Possible multi-region deployment in the future
- Database sharding, replication, and data migration between environments
- Strong operational and security requirements (observability, auditability)
How can we generate primary keys that support these constraints while keeping performance,
operational simplicity, and safety in mind?
## Decision Drivers
- Support independent ID generation across services (no central coordinator)
- Safe data import/export and merging across environments
- Reasonable storage and index performance
- Avoid leaking sequential or guessable identifiers
- Simplicity of implementation and ecosystem support
## Considered Options
- Option 1: Auto-increment integers (SERIAL in PostgreSQL) β Simple and compact but requires
centralized coordination (per-database) and causes collisions when merging datasets.
- Option 2: UUIDs (v4) β Globally unique, easy to generate anywhere, widely supported; larger
storage footprint and less human-friendly in logs.
- Option 3: ULID β Lexicographically sortable ID with good uniqueness and somewhat smaller
footprint than UUIDs; requires extra libraries in some stacks.
- Option 4: Snowflake-style IDs β Compact, sortable, and efficient but requires coordinated
generators and operational infrastructure (time/node bits).
## Decision Outcome
Chosen option: Option 2 β UUIDs (v4).
Rationale: We chose UUIDs (v4) because they let services generate identifiers locally without
a central coordinator, eliminate collision risk when merging data across environments, and
have broad ecosystem support. UUIDs increase storage and index size and are less humanβ
friendly in logs, but those tradeoffs are acceptable given our distributed, multiβregion,
and dataβmigration requirements. We will mitigate readability concerns using short aliases
or adminβfacing identifiers where appropriate.
### Consequences
* β
Services can generate IDs locally with no coordination
* β
Safe merging/importing of data between environments
* β Larger storage and index size compared with 32-bit integers
* β Less human-friendly IDs in logs and UIs (mitigations: short aliases or readable keys)
## More Information
### Implementation Examples
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL
);
...
### Review Schedule
- **Next Review:** 2026-07-12 (6 months)
The full ADR contains implementation notes, examples, and mitigation strategies β detailed enough to be useful, yet concise enough to read in a review. It captures the rationale you need to understand and act on past decisions.
Why MADR (and plain-text ADRs) work
Structured: A consistent template makes information predictable and easy to scan.
Discoverable: Plain text is searchable by IDEs, grep, and AI tools, so decisions are straightforward to find.
Versioned: ADRs live in git; the rationale and its changes are preserved in commits and PR history.
Reviewable: ADRs can be reviewed in pull requests alongside the code they affect.
Actionable: ADRs link to implementation notes, follow-up tasks, or specs, closing the loop between decisions and code.
ADR Organization
architecture/
βββ adr/
β βββ README.md # Index with status table
β βββ 0001-use-uuid-primary-keys.md
β βββ 0002-microservices-architecture.md
β βββ 0003-event-driven-communication.md
The README includes a status table:
| ADR | Title | Status | Date |
|---|---|---|---|
| ADR-0001: Use UUIDs for Primary Keys | Use UUIDs for Primary Keys | Accepted | 2026-01-12 |
| ADR-0002: Microservices Architecture | Adopt Microservices Architecture | Accepted | 2026-01-15 |
| ADR-0003: Event-Driven Communication | Use Event-Driven Communication | Accepted | 2026-01-20 |
This makes it trivial to see all decisions at a glance.
C4 Diagrams: Zoom Levels for Architecture
Diagrams and ADRs solve different problems: diagrams show structure and behavior; ADRs explain the decisions that produced that structure. Use the C4 model to present the system at multiple, targeted zoom levels so each audience gets the right information without overload.
What is C4?
The C4 model defines four diagram levels that act like a geographic mapβyou can zoom in or out depending on the reader's needs:
System Context (10,000-foot view) β The system, its users, and external dependencies. Useful for non-technical stakeholders and new team members who need the big picture.
Container (1,000-foot view) β The major applications and services (web apps, APIs, databases, message brokers) and how they interact. Suited for architects, senior developers, and DevOps engineers.
Component (100-foot view) β Internal components within a container and their relationships. Intended for developers working on a specific service.
Code (10-foot view) β Code-level structure (classes, modules); usually only needed for detailed design or maintenance tasks.
Each level answers different questions for different audiencesβkeep each view focused, link between levels, and cross-reference ADRs where decisions affect structure.
Supporting diagrams
Beyond the four C4 levels, three supporting diagram types are often helpful:
System landscape β shows the product in the broader ecosystem (other systems, partners); audience: executives and crossβteam stakeholders.
Dynamic (sequence / interaction) β scenario-based flow diagrams that show runtime interactions for specific use cases; audience: developers and architects.
Deployment β runtime topology showing environments, nodes, and network boundaries; audience: SRE/ops and architects.
Keep each view focused, cross-link diagrams to relevant ADRs, and include only the views that add clarity for their intended audience.
Level 1: System Context
Question: What is this system and how does it relate to the world?
Audience: Executives, product managers, new team members
workspace "E-Commerce System" {
model {
customer = person "Customer"
admin = person "Administrator"
paymentGateway = softwareSystem "Payment Gateway" "External"
emailSystem = softwareSystem "Email Service" "External"
ecommerceSystem = softwareSystem "E-Commerce Platform" {
...
}
customer -> ecommerceSystem "Browses products, places orders"
ecommerceSystem -> paymentGateway "Processes payments via"
ecommerceSystem -> emailSystem "Sends emails via"
}
views {
systemContext ecommerceSystem {
include *
autoLayout
}
}
}
π View the diagram online
This view gives the 10,000βfoot perspective: the system's actors, external dependencies (for example, payment and email services), and how users interact with the platform.
Level 2: Container
Question: What are the primary applications and services, what responsibilities do they hold, and how do they communicate?
Audience: Architects, senior developers, and SRE/DevOps
workspace "E-Commerce System - Container View" {
model {
customer = person "Customer"
ecommerceSystem = softwareSystem "E-Commerce Platform" {
webApp = container "Web Application" "React, TypeScript"
apiGateway = container "API Gateway" "Kong, NGINX"
customerService = container "Customer Service" "Node.js"
orderService = container "Order Service" "Node.js"
productService = container "Product Service" "Node.js"
paymentService = container "Payment Service" "Node.js"
messageBroker = container "Message Broker" "RabbitMQ"
customerDB = container "Customer Database" "PostgreSQL"
orderDB = container "Order Database" "PostgreSQL"
...
}
customer -> webApp "Uses"
webApp -> apiGateway "Calls"
apiGateway -> customerService "Routes to"
...
orderService -> messageBroker "Publishes OrderCreated"
messageBroker -> productService "Delivers OrderCreated"
}
}
π View the diagram online
This shows microservices, databases, the message broker, and both synchronous (REST) and asynchronous (events) communication.
Level 3: Component
This view digs into a single service to show its internal building blocks: components, their responsibilities, and the interactions (sync calls or events) between them. Use the component view to explain how the service implements business logic, how it persists state, and which components publish or subscribe to events.
Question: What components make up this service and how do they collaborate to implement its features?
Audience: Developers and maintainers working on a specific service
Below is a simplified component view for the Order Service that highlights controllers, business logic, repositories, and event publishers/subscribers.
Here's the Order Service component view (simplified):
workspace "Order Service - Component View" {
model {
orderService = softwareSystem "Order Service" {
orderController = container "Order Controller" "REST API"
orderManager = container "Order Manager" "Business Logic"
orderValidator = container "Order Validator" "Validation"
orderStateMachine = container "Order State Machine"
orderRepository = container "Order Repository" "Data Access"
orderEventPublisher = container "Event Publisher"
orderEventSubscriber = container "Event Subscriber"
...
}
orderController -> orderValidator "Validates order"
orderController -> orderManager "Creates order"
orderManager -> orderRepository "Saves order"
orderManager -> orderEventPublisher "Publishes OrderCreated"
...
}
}
π View the diagram online
This shows the internal architecture: controllers, business logic, repositories, and event handlers.
Level 4: Code and Deployment
We rarely create Code-level C4 diagrams β class/module diagrams are usually a better fit for code structure. C4's Deployment diagrams, however, are useful for describing infrastructure topology, deployment targets, network boundaries, and runtime concerns that matter to operations and SRE.
For this article we stop at the Component level and point readers to domain models for entity and workflow details.
Why Structurizr DSL?
Mermaid is great for quick diagrams and domain models. For C4 diagrams at scale, however, Structurizr DSL provides stronger semantics and a smoother authoring workflow. Key benefits:
C4-native: expresses C4 concepts directly, so intent maps cleanly to the model
Consistent styling: generates professional, uniform diagrams with minimal manual layout
Flexible export: export to SVG, PNG, PlantUML, or view diagrams in the Structurizr Playground
Auto-layout: keeps diagrams tidy as they grow, reducing manual positioning work
The DSL files live in your repository as plain text, just like Mermaid:
architecture/
βββ c4-views/
β βββ structurizr-dsl/
β β βββ system-context.dsl
β β βββ container.dsl
β β βββ order-service-component.dsl
β β βββ payment-service-component.dsl
β βββ images/
β β βββ system-context.svg
β β βββ container.svg
β β βββ ...
β βββ system-context.md
β βββ container.md
β βββ README.md
You edit the .dsl files, generate .svg images, and embed them in Markdown wrappers for navigation.
Note on SVG: SVG files are text-based (XML), scalable, and programmable (π€β€οΈ)βyou can further customize them or embed them directly in your documentation. Unlike PNG (binary and pixelated), SVG diagrams look sharp at any size, take up minimal space, and integrate seamlessly with version control and AI tooling.
The Complete Structure
Putting it all together, here's a production-grade architecture documentation structure:
architecture/
βββ README.md # Overview and navigation
βββ adr/ # Architecture Decision Records
β βββ README.md
β βββ 0001-use-uuid-primary-keys.md
β βββ 0002-microservices-architecture.md
β βββ 0003-event-driven-communication.md
βββ c4-views/ # C4 model views
β βββ README.md
β βββ structurizr-dsl/
β β βββ system-context.dsl
β β βββ container.dsl
β β βββ order-service-component.dsl
β β βββ payment-service-component.dsl
β βββ images/
β β βββ *.svg
β βββ system-context.md
β βββ container.md
β βββ order-service-component.md
β βββ payment-service-component.md
βββ models/ # Domain models and workflows
βββ README.md
βββ domain/
β βββ customer.md
β βββ order.md
β βββ product.md
β βββ payment.md
βββ flows/
βββ create-order.md
βββ payment-processing.md
βββ inventory-management.md
Navigation Pattern
Every README acts as a navigation hub:
/architecture/README.mdβ Links to ADRs, C4 views, and models/architecture/adr/README.mdβ Status table of all decisions/architecture/c4-views/README.mdβ Links to all view levels/architecture/models/README.mdβ Links to domain and flows
Tip: Put a
README.mdin every architecture folder β GitHub shows a folder's README when you open the folder, so the README acts as a small navigation hub that gives immediate context and links.
Crossβlink documents so readers can jump between related artifacts. For example:
## Related documentation
- [Container view](../../c4-views/container.md) β shows this service in context
- [ADRβ0003: EventβDriven Communication](../../adr/0003-event-driven-communication.md)
- [Order domain model](../domain/order.md)
This pattern creates a simple, navigable web of knowledge.
Other sections to consider as the architecture matures
requirements.mdβ highβlevel functional and nonβfunctional requirementsglossary.mdβ domain terms and ubiquitous languagedeployment/β infrastructure-as-code, environment configuration, and deployment patternssecurity/β security policies, authentication/authorization patterns, and threat modelstesting/β architecture validation, fitness functions, and integration checks
These files are omitted from the condensed example to keep the repo focused, but they follow the same principles: plain text, versionβcontrolled, AIβreadable, and modular.
βοΈ What's Next: Automation
Manually converting Structurizr DSL into SVGs doesn't scale. In the follow-up article I'll present a compact automation pipeline that links two parts: GitHub Copilot's Agent Skills to parse Structurizr DSL and emit diagram files, and GitHub Actions to run the generator on pushes and pull requests.
The pipeline will generate SVGs from DSL, refresh the Markdown wrappers that embed those diagrams, and (optionally) commit the generated assets back to the repository. The result is straightforward: diagrams regenerate automatically as the model evolves, keeping visuals and documentation aligned with minimal manual effort.
π Connecting Architecture to Code
Architecture documentation only helps when it guides implementation. OpenSpec provides a practical feedback loop that turns ADRs and C4 diagrams into verifiable specifications.
When your architecture lives as code, you can translate decisions and models into OpenSpec proposals and then validate implementation against those proposals. A typical flow looks like this:
An ADR is approved (for example: "Use EventβDriven Communication")
Create an OpenSpec change proposal describing how the decision will be implemented
Implement the change in the codebase
Run OpenSpec validation to confirm the code matches the proposal
Keep ADRs, specs, and code synchronized as the system evolves
This pattern works for both Greenfield projects (decide first, then implement) and Brownfield systems (implement gradually and validate as you refactor). For example, after approving ADRβ0003 you might create an OpenSpec proposal that specifies:
which services publish which events
the event schema and payload structure
which services should subscribe to each event
error-handling, retry, and delivery expectations
Developers implement the changes guided by both the ADR (the why) and the OpenSpec proposal (the how). OpenSpec then verifies the implementation, and the results feed back into the architecture records, closing the loop:
Architecture β OpenSpec β Code β Verified Against OpenSpec β Back to Architecture
For a detailed walkthrough of how the three pillars (ADRs, C4 diagrams, and domain models) come together in this feedback loop, see The Three-Pillar Approach and The Complete Loop in the companion repository.
The Complete Example
I've published a companion repository that contains the full, working example:
π architecture-as-code-example
The repo includes a ready-to-use set of artifacts you can fork or adapt:
Three ADRs in MADR format
Four C4 views (Structurizr DSL files and generated diagrams)
Five domain models with Mermaid diagrams
Three workflow/sequence diagrams
A complete navigation structure and README
Clone the repository, adapt the content to your project, and make it yours.
Final Thoughts
This approach is more than documentation β it's executable architecture.
Rather than storing decisions in wikis and diagrams in external tools, keep ADRs, C4 views, and domain models as plain text in the repository. That makes architecture discoverable, reviewable, versioned, and actionable by both people and AI.
Why this matters
For Architects & Senior/Principal Developers
You can create a feedback loop that closes the gap between design and implementation:
ADRs capture decisions and rationale β the context teams and AI need to make changes safely.
C4 diagrams describe structure at multiple zoom levels, so intent is visible to the right audience.
OpenSpec converts decisions and models into machine-readable specifications.
Code validation checks the implementation against those specifications.
Close the loop β validation results and changes feed back into ADRs and specs so the architecture and code stay aligned.
This is: Architecture β OpenSpec β Code β Verified Against OpenSpec β Back to Architecture β instrumented for AI.
AI as Your Architecture Assistant
When architecture is kept as plain text, AI agents can:
Analyze ADRs and propose matching OpenSpec change proposals
Compare code changes against architectural decisions and specifications
Recommend architecture updates as the codebase evolves
Auto-generate updated diagrams when models or DSL files change
Keep documentation and implementation synchronized with minimal manual effort
Together these capabilities turn architecture from a passive artifact into an active, machine-readable specification that guides development and verifies the implementation.
The Scalability Problem Solved
As systems grow, traditional documentation approaches break down: wikis rot, diagram tools fall out of sync, decisions vanish in PR noise, and new team members waste time hunting for context.
Plainβtext "architecture as code" fixes this.
When architecture lives in the repositoryβversioned with git, reviewed in PRs, and searchable by humans and AIβyour documentation scales with the system. New services, decisions, models, and diagrams become firstβclass, linkable artifacts that evolve alongside the codebase.
The Next Frontier
This is just the beginning. With Agent Skills and automation via GitHub Actions, you can:
Generate C4 diagrams (SVG) automatically on every PR or merge
Validate code continuously against OpenSpec proposals
Surface architecture improvements from code patterns and tests
Keep architecture and implementation continuously in sync
Teams that adopt this pipeline gain a practical edge: architecture becomes executable, auditable, and selfβmaintaining.
Try it: feed your ADRs and C4 models into OpenSpec, run the automation, and watch what happens when architecture is code that AI can reason about.
That's the future of executable architecture. π
π References and Further Reading
Architecture Decision Records (ADRs)
ADR.github.io β Official ADR documentation and templates
MADR Template β Markdown Architectural Decision Records specification
Documenting Architecture Decisions β Original Michael Nygard article
C4 Model
C4 Model β Official C4 model documentation and examples
Structurizr β C4 tooling and DSL documentation
Structurizr Playground β Online editor for DSL files
Domain-Driven Design & Models
Domain-Driven Design β Eric Evans' foundational work
Ubiquitous Language β Martin Fowler's explanation
Mermaid Diagrams
Mermaid.js β Diagram syntax and examples
Mermaid in Markdown β Embedding diagrams in Markdown
Specification & Validation
OpenSpec β Machine-readable specifications for code validation
Fitness Functions β Verifying architecture through automated checks
Tools & Automation
GitHub Actions β CI/CD automation
GitHub Copilot Agent Skills β Custom agents for code generation
Previous Articles:
Repository: architecture-as-code-example



