Home Blog Developer Experience and Pl Architecture fitness functions in CI/CD pipelines
Developer Experience and Pl May 27, 2026 7 min read

Architecture fitness functions in CI/CD pipelines

Developer Experience and Pl Enterprise Guide 2026 SCALE D2C D2C Technology Developer Experience and Pl Enterprise Guide 2026 SCALE D2C D2C Technology

Architecture fitness functions are automated tests that verify architectural constraints are not violated as a codebase evolves. Embedded in CI/CD pipelines, they prevent the gradual erosion of architectural integrity that plagues long-lived systems β€” providing the same safety net for architecture that unit tests provide for business logic.

What Are Architecture Fitness Functions?

The term "fitness function" comes from evolutionary architecture, introduced by Neal Ford, Rebecca Parsons, and Patrick Kua in "Building Evolutionary Architectures" (O'Reilly). A fitness function is any mechanism that evaluates how well the system conforms to a desired architectural property β€” dependency structure, performance characteristics, security posture, code complexity, or compliance with naming and layering conventions.

Definition
An architecture fitness function is an automated test that evaluates whether the system's architecture satisfies a defined structural, behavioural, or quality constraint β€” run in CI/CD to prevent architectural drift and enforce the "architecture as code" principle.

The power of fitness functions is that they make implicit architectural rules explicit and enforceable. "The service layer should not depend on the presentation layer" is a common architectural rule that lives in architecture documentation but is routinely violated as codebases grow. A fitness function turns that rule into a failing test that blocks deployment.

Categories of Architecture Fitness Functions

πŸ—οΈ
Structural Constraints
Enforce layered architecture rules, module dependency graphs, and package import restrictions. Examples: domain layer must not import from infrastructure layer; circular dependencies are forbidden; public API surface must not exceed defined module count.
⚑
Performance Thresholds
Assert that critical API endpoints respond within defined SLOs, bundle sizes remain within budget, and database query counts per request don't exceed limits. Run as part of CI against a performance test environment.
πŸ”
Security Properties
Verify that all API endpoints require authentication, sensitive data fields are encrypted at rest, secrets are not committed to source code, and third-party dependencies don't have known critical vulnerabilities.
πŸ“
Complexity Metrics
Measure cyclomatic complexity, cognitive complexity, class/function size, and coupling metrics. Alert or fail builds when complexity exceeds thresholds, preventing the gradual accumulation of complex code that becomes unmaintainable.
πŸ“‹
Naming and Convention Conventions
Enforce naming conventions, file structure standards, and code organisation patterns. Ensures new code follows established patterns even as teams grow and change composition.
πŸ”„
API Contract Compliance
Verify that API contracts (OpenAPI specs, Protobuf schemas, event schemas) are not broken by changes. Consumer-driven contract tests ensure downstream services are not broken by provider changes.

Tooling for Architecture Fitness Functions

ToolLanguage/EcosystemPrimary Use Case
ArchUnitJava / KotlinPackage dependency rules, layer constraints, naming conventions
NetArchTest.NET / C#Namespace dependency rules, DDD layer enforcement
Dependency CruiserJavaScript / TypeScriptModule dependency graph rules, circular dependency detection
ESLint import/no-restricted-importsJavaScript / TypeScriptImport restriction rules enforced at lint time
SonarQube / SonarCloudMulti-languageComplexity metrics, technical debt thresholds, code smell limits
SpectralAPI / OpenAPIAPI design rule enforcement (naming, versioning, response formats)
PactMulti-languageConsumer-driven contract testing for microservices
Bundlesize / size-limitJavaScriptBundle size budget enforcement

ArchUnit Example: Enforcing Layered Architecture

ArchUnit is the most widely adopted fitness function tool for Java systems. It allows you to write architectural rules as JUnit tests:

@ArchTest
static final ArchRule domainMustNotDependOnInfrastructure =
    noClasses().that().resideInAPackage("..domain..")
        .should().dependOnClassesThat()
        .resideInAPackage("..infrastructure..");

@ArchTest
static final ArchRule servicesMustNotAccessRepositoriesDirectly =
    noClasses().that().resideInAPackage("..service..")
        .should().accessClassesThat()
        .areAnnotatedWith(Repository.class)
        .because("services should use repositories through interfaces only");

@ArchTest
static final ArchRule noCircularDependencies =
    slices().matching("com.example.(*)..").should().beFreeOfCycles();

These tests run as part of the standard Maven/Gradle test suite and fail the build if violations are introduced, preventing architectural drift before it reaches production.

Dependency Cruiser for TypeScript/JavaScript

For TypeScript projects, Dependency Cruiser provides equivalent capabilities through a rules configuration file:

// .dependency-cruiser.cjs
module.exports = {
  forbidden: [
    {
      name: "no-circular",
      severity: "error",
      comment: "Circular dependencies are forbidden",
      from: {},
      to: { circular: true }
    },
    {
      name: "domain-no-infra",
      severity: "error",
      from: { path: "^src/domain" },
      to: { path: "^src/infrastructure" }
    }
  ]
};

Integrating Fitness Functions into CI/CD

01
Start with Audit Mode
Run your first fitness functions in "audit only" mode β€” report violations without failing the build. This reveals the current state of architectural drift without immediately breaking CI for every team member.
02
Fix and Baseline
Address the violations discovered in audit mode, or explicitly baseline them as known exceptions that will be resolved over time. Only enforce rules where the baseline is clean β€” don't create a CI pipeline that always fails.
03
Enable Enforcement
Switch fitness functions to enforcement mode (fail the build on violation). Place them in the pull request validation stage so violations are caught before merge.
04
Document and Communicate
Document each fitness function in the engineering handbook: what rule it enforces, why it exists, and the exception process if a developer needs to override it. Undocumented fitness functions create confusion and resistance.

Frequently Asked Questions

An architecture fitness function is an automated test that evaluates whether the system's architecture satisfies a defined constraint β€” dependency rules, performance thresholds, security properties, or complexity limits. The term comes from evolutionary architecture theory. Fitness functions make implicit architectural rules explicit and machine-enforceable: instead of relying on code review to catch layering violations, a fitness function fails the CI build when the domain layer imports from the infrastructure layer, preventing architectural drift before it reaches the main branch.

ArchUnit is a Java/Kotlin library for writing architectural rules as unit tests. It works by analysing the compiled bytecode of your application and evaluating rules expressed in a fluent DSL: "no classes in the domain package should depend on classes in the infrastructure package." These rules run as JUnit tests in your standard test suite, integrating naturally with Maven/Gradle builds and CI/CD pipelines. ArchUnit supports rules for package dependencies, class naming conventions, annotation requirements, circular dependency detection, and layered architecture enforcement.

For TypeScript and JavaScript projects, Dependency Cruiser is the primary tool for module dependency rules and circular dependency detection. It analyses import graphs and applies rules defined in a configuration file, failing the build when rules are violated. ESLint's import rules (import/no-restricted-imports, import/no-cycle) provide lighter-weight enforcement directly in the linting pipeline. For bundle size budgets, size-limit or bundlesize enforce JavaScript bundle constraints. SonarCloud covers complexity metrics across multiple languages including TypeScript.

Clean architecture (and hexagonal architecture, onion architecture) define layering rules: domain logic in the center with no outward dependencies; application services depending on domain; infrastructure adapters depending on application interfaces but never the reverse. Fitness functions enforce these rules automatically. ArchUnit's noClasses().that().resideInAPackage("..domain..").should().dependOnClassesThat().resideInAPackage("..infrastructure..") directly encodes the clean architecture dependency rule as a failing test, preventing the rule from being quietly violated as the codebase grows.

Start with the highest-value, lowest-effort fitness functions: circular dependency detection (Dependency Cruiser or ArchUnit's beFreeOfCycles()) β€” circular dependencies are universally harmful and easy to detect automatically; layer dependency rules encoding your architectural boundaries; bundle size budgets if you have performance-sensitive frontend code; and critical security properties like "all API endpoints must require authentication." These provide immediate architectural safety net value and are straightforward to implement. Save complex performance SLO testing and consumer contract tests for later phases when the basic structure is protected.

Yes, fitness functions apply to both. For microservices, the most valuable fitness functions are: consumer-driven contract tests (Pact) that prevent API changes from breaking downstream services; API design rule enforcement (Spectral on OpenAPI specs) ensuring consistent naming, versioning, and error formats across services; dependency scanning for known vulnerabilities in service dependencies; and performance SLO tests that assert service response times meet requirements. For monorepos containing multiple services, Dependency Cruiser and ArchUnit can enforce that services don't import directly from each other's internals, maintaining service boundary integrity.

Consumer-driven contract testing is a pattern where API consumers define the contract they expect (the specific request and response format they rely on), and provider CI/CD pipelines verify they still satisfy those contracts before deployment. Pact is the most widely used framework for this pattern. Consumers write Pact tests that generate contract files; providers run Pact verification tests against those contracts. When a provider change would break a consumer contract, the provider's CI build fails before the breaking change is deployed. This gives service teams confidence to evolve their APIs without inadvertently breaking downstream consumers.

Legitimate exceptions require a formal process to prevent the exception mechanism from becoming a workaround for the rule. For ArchUnit, use .ignoreDependency() to document specific known exceptions with an explanation comment. For Dependency Cruiser, use the allowedSeverity configuration or path-specific rule exceptions. For ESLint, use targeted // eslint-disable-next-line with a comment explaining the exception. The key principle is that every exception must be explicit and documented β€” not a blanket rule disable. Track exceptions as technical debt items for future resolution, and review them in quarterly architecture reviews.

ARCHITECTU

Ready to Implement Architecture fitness functions in CI/CD pipelines?

Our specialist team delivers measurable ROI from Developer Experience and Pl programmes for enterprise and D2C brands.

Free Audit