Architecture as Code: Define Your System Design Programmatically - Archyl Blog

Architecture as code brings the same rigor that infrastructure-as-code brought to operations. This guide covers what AaC is, why it beats visual-only approaches, real YAML and DSL examples, version control workflows, CI/CD integration, and how Archyl makes it practical.

Architecture as Code: Define Your System Design Programmatically

There's a pattern in software engineering that repeats itself every few years. A practice that was manual and visual gets codified, version-controlled, and automated -- and everything gets better.

It happened with infrastructure. We went from clicking through cloud consoles to writing Terraform files. It happened with configuration. We went from editing config files on servers to declaring desired state in Kubernetes manifests. It happened with database schemas. We went from running SQL scripts by hand to writing migration files.

Now it's happening with architecture documentation. Architecture as code is the practice of defining your system design programmatically -- in structured text files that can be version-controlled, reviewed, tested, and deployed through the same pipelines as your application code.

This guide covers everything you need to know about architecture as code: what it is, why it matters, how it compares to visual-only approaches, and how to implement it in practice.

What Is Architecture as Code?

Architecture as code (AaC) is the practice of defining your software architecture in machine-readable, human-writable text files. Instead of drawing boxes and arrows in a visual tool, you describe your systems, containers, components, and their relationships in a structured format like YAML, JSON, or a purpose-built DSL.

Here's a simple example of an architecture defined in YAML:

version: "1.0"

project:
  name: "Payment Platform"
  description: "Handles all payment processing for the organization"

systems:
  - name: Payment Platform
    type: software_system
    description: "Core payment processing system"
    containers:
      - name: Payment API
        type: api
        description: "REST API for payment operations"
        technologies: [Go, gRPC, OpenAPI]
      - name: Payment Processor
        type: service
        description: "Processes payment transactions"
        technologies: [Go]
      - name: Transaction Database
        type: database
        description: "Stores transaction records"
        technologies: [PostgreSQL]
      - name: Payment Queue
        type: queue
        description: "Async payment processing queue"
        technologies: [Kafka]

  - name: Stripe
    type: external_system
    description: "Third-party payment gateway"

relationships:
  - from: Payment API
    to: Payment Processor
    label: "Forwards payment requests"
    type: uses
  - from: Payment Processor
    to: Transaction Database
    label: "Persists transactions"
    type: writes_to
  - from: Payment Processor
    to: Payment Queue
    label: "Publishes payment events"
    type: publishes_to
  - from: Payment Processor
    to: Stripe
    label: "Charges cards via"
    type: uses

This file is the complete source of truth for the architecture. A tool like Archyl reads it, builds the C4 model, renders interactive diagrams, and keeps everything synchronized. The file lives in your Git repository, right next to the code it describes.

Why Visual-Only Approaches Fall Short

Before architecture as code, teams typically documented their architecture using visual tools -- Lucidchart, draw.io, Miro, or Figma. These tools are excellent for brainstorming and initial design sessions, but they have fundamental limitations as long-term documentation:

No Version Control

Visual diagrams are stored as binary or proprietary files that can't be meaningfully diffed. When someone changes a diagram, you can see that it changed, but you can't see what changed. There's no equivalent of git diff for a draw.io file. You can't review a diagram change in a pull request the way you review a code change.

With architecture as code, every change is a text diff. Adding a new service is a few lines of YAML. Renaming a component is a single-line change. Reviewers can see exactly what changed, why it changed (from the commit message), and approve or request modifications.

No Automation

Visual diagrams exist in isolation. They can't trigger actions, validate rules, or integrate with CI/CD pipelines. If your diagram says you have 10 services but your Kubernetes cluster runs 12, nothing detects the discrepancy.

Architecture as code enables automation. You can write validation rules that check your architecture definition against your actual infrastructure. You can generate documentation from the architecture file. You can trigger alerts when the architecture file diverges from reality.

No Collaboration at Scale

When two people edit the same visual diagram simultaneously, conflicts are usually resolved by one person's changes overwriting the other's. There's no merge strategy for visual files.

With architecture as code, standard Git merge workflows apply. Two teams can modify different parts of the architecture file, and Git merges them cleanly. When conflicts do occur, they're resolved the same way code conflicts are -- through discussion and intentional resolution.

No Consistency Guarantees

A visual diagram can contain anything. Boxes can be labeled inconsistently. Arrows can mean different things in different parts of the same diagram. There's no schema, no validation, no enforcement of naming conventions.

Architecture as code files have a schema. The tooling validates the file on every change. If you reference a container that doesn't exist, the validation catches it. If you use an invalid relationship type, it's flagged before the change is merged.

Lock-in and Portability

Visual diagrams are often locked into the tool that created them. Moving from Lucidchart to draw.io means manually recreating every diagram. Moving from one architecture-as-code tool to another is a format conversion -- automated and repeatable.

The Benefits of Architecture as Code

Single Source of Truth

When your architecture is defined in a single file (or a set of files), there's exactly one place to look. There's no question about which diagram is current, which Confluence page has the latest version, or whether the PDF someone emailed last month is still accurate.

Code Review for Architecture Changes

This is perhaps the most transformative benefit. When architecture changes go through pull requests, they get the same scrutiny as code changes. A senior architect can review a proposed service split before it happens. The team can discuss the implications of a new dependency before it's introduced.

+ - name: Notification Service
+   type: service
+   description: "Handles email, SMS, and push notifications"
+   technologies: [Python, Celery, Redis]
+
+ - from: Order Service
+   to: Notification Service
+   label: "Triggers order notifications"
+   type: uses

This diff tells a clear story: someone is adding a Notification Service and connecting it to the Order Service. Reviewers can ask questions, suggest alternative technologies, or propose different service boundaries -- all before a single line of application code is written.

Git History Is Architecture History

Every commit to your architecture file creates a permanent record of how the architecture evolved. You can answer questions like:

  • When was the Search Service added?
  • Who approved the migration from MySQL to PostgreSQL?
  • What did the architecture look like six months ago?
  • How has the number of services grown over time?

This history is invaluable for understanding the evolution of your system and for onboarding new team members.

CI/CD Integration

Architecture as code integrates naturally into continuous integration and continuous deployment pipelines. On every pull request, you can:

  • Validate the architecture file against its schema
  • Check conformance rules (e.g., every service must have a documented owner)
  • Generate updated diagrams
  • Detect drift between the documented architecture and the running system
  • Publish the architecture to your documentation platform

This makes architecture documentation a living artifact rather than a static document that decays.

Refactoring and Automation

Because architecture definitions are structured data, you can write scripts to manipulate them. Need to rename a service across all relationships? A simple find-and-replace in a YAML file. Need to generate a report of all services using PostgreSQL? Parse the YAML and filter by technology. Need to enforce a naming convention? Write a linter.

Architecture as Code Formats and DSLs

Several formats and DSLs exist for defining architecture as code. Here's an overview of the most common approaches.

Structurizr DSL

Created by Simon Brown (the creator of the C4 model), Structurizr DSL is one of the earliest architecture-as-code formats. It uses a custom DSL syntax:

workspace {
    model {
        user = person "User"
        softwareSystem = softwareSystem "My Software System" {
            webapp = container "Web Application" "Delivers content" "Java"
            database = container "Database" "Stores data" "PostgreSQL"
        }
        user -> webapp "Uses"
        webapp -> database "Reads from and writes to"
    }
    views {
        systemContext softwareSystem {
            include *
            autolayout lr
        }
    }
}

Structurizr pioneered the concept of architecture as code for C4 models. However, its custom DSL syntax has a learning curve, and it requires Structurizr-specific tooling to render.

YAML-Based Approaches

YAML has become the de facto standard for declarative configuration in DevOps (Kubernetes, Docker Compose, GitHub Actions, Terraform HCL notwithstanding). Using YAML for architecture definitions has the advantage of familiarity -- most developers already know how to read and write YAML.

Archyl's archyl.yaml format takes this approach:

version: "1.0"

systems:
  - name: E-Commerce Platform
    type: software_system
    containers:
      - name: Web Frontend
        type: webapp
        technologies: [React, TypeScript, Next.js]
      - name: API Service
        type: api
        technologies: [Go, gRPC]
        components:
          - name: Auth Handler
            type: handler
            technologies: [JWT, OAuth2]
          - name: Product Handler
            type: handler
            technologies: [REST]
      - name: Product Database
        type: database
        technologies: [PostgreSQL]

relationships:
  - from: Web Frontend
    to: API Service
    label: "Makes API calls to"
  - from: API Service
    to: Product Database
    label: "Reads/writes product data"

The nesting mirrors the C4 hierarchy directly: systems contain containers, containers contain components. Relationships use human-readable names with dot notation for disambiguation. The format is greppable, diffable, and requires no specialized tooling to read.

JSON and Other Formats

Some tools use JSON, TOML, or other structured formats. The specific format matters less than the principles: the architecture definition should be text-based, version-controllable, and machine-parseable.

Implementing Architecture as Code: A Practical Workflow

Here's a step-by-step workflow for adopting architecture as code in your team.

Step 1: Start with What Exists

Don't try to document your entire architecture on day one. Start with the Container diagram -- the service landscape. List every deployable service, its technology stack, and the key relationships between services.

If you're using Archyl, you can either create the model visually in the UI and then export it as archyl.yaml, or write the YAML file from scratch. Either path gets you to the same result.

Step 2: Commit to Your Repository

Place the architecture file at the root of your primary repository (or in a dedicated architecture repository if your codebase is split across many repos). The location matters less than the principle: the file should live in Git and go through code review.

my-platform/
  archyl.yaml        # Architecture definition
  src/
  docker-compose.yml
  .github/
    workflows/
      architecture.yml  # CI pipeline for architecture

Step 3: Set Up CI/CD Sync

Configure your CI/CD pipeline to sync the architecture file with Archyl on every merge to the main branch. This ensures that the visual diagrams and interactive documentation in Archyl always reflect the latest committed architecture.

A GitHub Actions workflow might look like:

name: Sync Architecture

on:
  push:
    branches: [main]
    paths: [archyl.yaml]

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Sync to Archyl
        run: |
          curl -X POST https://api.archyl.com/v1/sync \
            -H "Authorization: Bearer ${{ secrets.ARCHYL_TOKEN }}" \
            -H "Content-Type: application/yaml" \
            --data-binary @archyl.yaml

Step 4: Make Architecture Changes Through Pull Requests

From this point forward, architecture changes follow the same workflow as code changes:

  1. Create a branch
  2. Modify the archyl.yaml file
  3. Open a pull request
  4. Get it reviewed by the team
  5. Merge to main
  6. CI/CD syncs the change to Archyl

This gives architecture changes the same visibility, accountability, and traceability as code changes.

Step 5: Add Conformance Rules

As your architecture-as-code practice matures, add conformance rules that validate the architecture definition automatically. Examples:

  • Every container must have at least one technology specified
  • Every external system must have a description
  • No orphan containers (every container must have at least one relationship)
  • Naming conventions are followed (e.g., services end with "Service")

Archyl's conformance rules engine can evaluate these rules automatically and report violations in the CI pipeline or in the Archyl dashboard.

Step 6: Evolve the Definition Over Time

Start with systems and containers. Add components when specific services become complex enough to warrant internal documentation. Add ADRs as you make important architectural decisions. Add API contracts as you formalize service boundaries.

The architecture file grows organically with your system. There's no need to front-load every detail.

Architecture as Code vs. Infrastructure as Code

Architecture as code and infrastructure as code (IaC) are complementary but distinct practices.

Infrastructure as code (Terraform, Pulumi, CloudFormation) defines what to deploy and how to configure it. It's operational: it provisions servers, configures networks, and manages cloud resources.

Architecture as code defines what the system looks like and how its parts relate. It's descriptive: it documents the conceptual structure, technology choices, and service boundaries.

The ideal setup combines both:

  • Your Terraform files define the infrastructure
  • Your archyl.yaml defines the architecture
  • Conformance rules check that the two stay aligned

When your Terraform adds a new service but the architecture file doesn't mention it, drift detection catches the discrepancy.

Architecture as Code with AI Assistants

One of the most compelling advantages of architecture as code is that AI assistants can read and reason about it. When your architecture is defined in structured text, tools like Claude Code and Cursor can:

  • Answer questions about your architecture by querying the YAML file
  • Suggest architecture changes based on the current state
  • Generate code that respects the documented architecture (e.g., using the right database for the right service)
  • Detect inconsistencies between the code and the architecture definition

Archyl takes this further with its MCP server. AI assistants don't just read the architecture file -- they can query the live architecture model, traverse relationships, and even propose modifications. The architecture becomes a programmable, queryable data source rather than a static document.

Common Pitfalls

Over-Engineering the Format

Don't design a custom DSL when YAML or an existing format works. The goal is adoption, and adoption is easier when the format is familiar. Most developers already know YAML from Docker Compose, Kubernetes, and CI/CD configurations.

Trying to Capture Everything

Architecture as code should capture the structural aspects of your system: what exists, how things connect, and what technologies are used. Don't try to embed operational details (like scaling policies), runtime configurations (like environment variables), or behavioral specifications (like API response formats) in the architecture file.

Not Enforcing the Workflow

Architecture as code only works if changes go through the defined workflow. If people bypass the architecture file and make changes directly in the visual tool, the file becomes stale. Establish clear conventions about which direction is authoritative.

Ignoring the Visual Output

Architecture as code is not a replacement for visual diagrams -- it's a better way to produce them. The text file is the source of truth, but the rendered diagrams are what people actually look at day-to-day. Make sure the visual output is accessible, up-to-date, and easy to navigate.

Getting Started with Archyl

Archyl is designed from the ground up to support architecture as code. The platform provides:

  • YAML-based DSL that covers the full C4 model with systems, containers, components, relationships, and technologies
  • Bidirectional sync -- model visually in the UI and export to YAML, or write YAML and sync to the UI
  • CI/CD integration for automated syncing on every commit
  • Conformance rules that validate the architecture definition against your standards
  • MCP server that makes the architecture queryable by AI assistants
  • Collaboration features with code review, comments, and team ownership

Whether you're starting from scratch or migrating from visual diagrams, Archyl makes architecture as code practical for teams of any size.

Get started with architecture as code and bring the same rigor to your architecture documentation that you already bring to your infrastructure and application code.