Architecture changes whether a team plans it or not. New dependencies enter the graph, temporary integrations become permanent, data ownership blurs, and deployment topology drifts away from diagrams. A design review can establish intent, but it cannot continuously inspect every pull request, database change, artifact, and running environment.

An architectural fitness function is an automated evaluation of a system characteristic that matters to its intended shape. It turns statements such as “domain code is independent of web frameworks,” “only the billing service writes billing data,” or “a region can operate when the control plane is unavailable” into repeatable evidence. Some functions are binary constraints; others measure a trend or threshold. Together they make evolution observable without freezing the system in its current form.

The goal is not to encode every preference as a blocking rule. It is to protect a small set of consequential qualities, assign their ownership, and revise the checks when the architecture deliberately changes. Fitness functions are valuable when they constrain outcomes while leaving teams room to alter implementations.

Turn Architectural Intent into Executable Evidence

A fitness function starts with a quality or boundary, not a tool. “Run a dependency linter” names machinery. “Prevent cycles between business modules so they can be changed and tested independently” names architectural intent. The latter can survive a language or build-system change because its evidence source can be replaced.

Write each function as a compact contract:

text
Characteristic: what quality or boundary matters?
Scope: which code, data, deployment, or traffic does it cover?
Evidence: what observable fact represents the characteristic?
Evaluation: how is pass, warning, or failure calculated?
Cadence: pull request, build, deployment, scheduled, or continuous?
Owner: who investigates and who may revise the rule?
Response: block, alert, create work, or record a trend?

For example, “Orders does not depend on Payments internals” can be evaluated from a compiler-derived import graph on every pull request. “Only Payments writes the payment schema” may require database grants plus audit evidence checked after deployment. “The service tolerates loss of one zone” cannot be established by static code; a scheduled failure exercise can evaluate observed behavior.

The evidence must be close enough to the property to be meaningful. Counting source directories does not prove modularity. Requiring test coverage does not prove replaceability. A latency benchmark does not prove availability during a zone failure. Proxy metrics can be useful trends, but label them honestly and pair them with a direct check where the risk justifies it.

Fitness functions differ from ordinary feature tests by the reason they exist. A feature test asks whether checkout calculates a total correctly. An architectural function might ask whether checkout can be built without importing a payment-provider SDK into the domain layer. The same test framework may run both, but the latter protects a system-wide design decision.

Build a Portfolio Across Architecture Surfaces

No single analyzer sees the whole architecture. A useful portfolio samples several surfaces and runs each function at the cheapest cadence that can catch meaningful drift.

Surface Example fitness function Evidence and cadence
Source dependencies Business modules have no cycles Compiler or analyzer graph on each change
API contracts Deployed consumers remain compatible Schema compatibility check before release
Data ownership Only the owning component writes a schema Grants plus database audit review continuously
Deployment Production artifacts are independently deployable where promised Artifact graph and deployment rehearsal
Resilience Loss of a declared dependency causes the documented degradation Controlled fault exercise on a schedule
Security boundary Internet-facing routes pass through required policy enforcement Route/topology inspection per deployment
Operability Every critical component emits required health and ownership metadata Manifest check at build and runtime inventory
Evolution Deprecated interface usage reaches zero by its removal date Repository and runtime usage trend

Balance leading and lagging evidence. Static dependency checks are fast and prevent a known violation before merge, but they cannot reveal reflective loading or runtime network calls. Production topology inspection catches deployed reality, but only after an artifact exists. Scheduled exercises provide stronger behavioral evidence at higher cost. The portfolio works because these modes cover one another’s blind spots.

Also balance binary and continuous functions. “No module cycles” is naturally pass or fail. Coupling, artifact size, cross-boundary call volume, or deprecated API use may be better represented as a trend with an agreed ceiling. Artificially converting every noisy measure into a hard gate creates flaky builds and arguments about measurement rather than architecture.

Limit the initial portfolio. Ten trusted functions tied to explicit decisions are more effective than hundreds of inherited rules nobody can explain. A function without an owner or response is merely a dashboard decoration. A function that always fails becomes background noise.

Work Through a Modular Commerce Platform

Consider a commerce platform organized as a modular monolith with catalog, orders, payments, and notifications modules. The intended architecture permits the application to deploy as one process today while preserving clear ownership and the option to extract a module later. The design has four important rules:

  1. A module may import another module’s public API, never its internal files.
  2. The module dependency graph must remain acyclic.
  3. Only payments code may issue writes against payment-owned tables.
  4. Domain packages must not import HTTP framework or vendor SDK packages.

A build tool can derive an import graph from the compiler’s resolved modules. A small evaluator then checks architectural meaning rather than parsing source with regular expressions. The following TypeScript is deliberately independent of a particular graph extractor:

ts
type Edge = Readonly<{
  from: string;
  to: string;
  importedPackage?: string;
}>;

type Violation = Readonly<{
  rule: string;
  from: string;
  to: string;
  explanation: string;
}>;

const businessModules = ['catalog', 'orders', 'payments', 'notifications'] as const;

function moduleOf(path: string): string | undefined {
  return businessModules.find((name) => path.startsWith(`src/${name}/`));
}

function isPublicApi(path: string, moduleName: string): boolean {
  return path === `src/${moduleName}/index.ts`
    || path.startsWith(`src/${moduleName}/api/`);
}

export function boundaryViolations(edges: readonly Edge[]): Violation[] {
  const violations: Violation[] = [];

  for (const edge of edges) {
    const fromModule = moduleOf(edge.from);
    const toModule = moduleOf(edge.to);

    if (fromModule && toModule && fromModule !== toModule && !isPublicApi(edge.to, toModule)) {
      violations.push({
        rule: 'MODULE_PUBLIC_API_ONLY',
        from: edge.from,
        to: edge.to,
        explanation: `${fromModule} imports ${toModule} internals`,
      });
    }

    const fromDomain = edge.from.includes('/domain/');
    const forbiddenPackage = edge.importedPackage === 'web-framework'
      || edge.importedPackage?.startsWith('payment-vendor-');
    if (fromDomain && forbiddenPackage) {
      violations.push({
        rule: 'DOMAIN_ISOLATION',
        from: edge.from,
        to: edge.to,
        explanation: 'domain code depends on delivery or vendor infrastructure',
      });
    }
  }
  return violations;
}

A separate graph algorithm checks cycles among module-level edges and prints the shortest discovered cycle, such as orders -> notifications -> payments -> orders. Actionable output matters: a bare “architecture failed” sends developers searching through the entire graph.

The data-ownership rule needs different evidence. Migration files are stored under an owning module, database roles grant payment-table writes only to the payment path, and a deployment check compares effective grants with a versioned ownership manifest. Runtime audit sampling detects direct writes from an unexpected identity. Static SQL search alone would miss dynamic queries and external administration, while grants alone would not reveal a shared superuser used by every module.

Now suppose orders currently writes one legacy payment status column. Turning the ownership rule on as an immediate global blocker would stop unrelated work without removing the dependency. Instead, establish a baseline exception for that exact edge, measure its runtime use, route new writes through the payments API, and require the exception count to move only downward. The fitness function protects the target architecture while allowing an incremental migration.

Choose Thresholds That Express Architectural Risk

A threshold should mark a meaningful loss of the characteristic, not merely a round number. For a dependency rule, any new cycle may be unacceptable because cycles undermine independent change immediately. For a gradual migration, the threshold may be “no new violating edges and at least one legacy edge removed by the milestone.” For a fault exercise, the threshold might be “read-only catalog remains available while payment creation fails closed.”

Classify evaluations by their certainty and consequence:

  • Invariant: deterministic and non-negotiable, so violation blocks change. Examples include forbidden dependency direction or an unowned public endpoint.
  • Guardrail: a threshold with understood variance, so a breach blocks deployment or requires explicit approval.
  • Trend: a directional measure used to detect erosion and create planned work.
  • Experiment: temporary evaluation used to learn whether a proposed measure represents the quality.

This vocabulary prevents a prototype metric from silently becoming permanent policy. Promote an experiment only after its signal is reproducible and teams know how to respond.

Thresholds need scope. “No more than five dependencies” is meaningless without defining dependency type, module size, and why six creates risk. A better rule limits imports across a stability boundary, or requires all external dependencies to enter through owned adapters. It constrains coupling that matters rather than rewarding code rearrangement.

Runtime measures require an observation window and sample quality. If the architecture promises that a control-plane outage does not interrupt existing data-plane traffic, evaluate behavior during an injected control-plane failure and inspect correctness as well as availability. A generic p95 CI budget cannot establish that promise. Performance may be one observed effect, but the fitness function is defined by degraded-operation semantics.

Review thresholds on a scheduled cadence and after material context changes. Tightening should be deliberate; loosening should require an architectural explanation. Preserve historical results so a passing binary gate does not conceal long-term movement toward its limit.

Manage Exceptions as Expiring Architecture Debt

Real systems rarely satisfy a newly articulated rule on day one. A blanket suppression makes the function ceremonial, while an uncompromising gate can make adoption impossible. Use a baseline or exception registry that identifies existing violations precisely and rejects new ones.

An exception record should contain:

yaml
rule: MODULE_PUBLIC_API_ONLY
from: src/orders/application/legacy-refund.ts
to: src/payments/internal/provider-map.ts
owner: commerce-payments
reason: Refund migration still requires the legacy provider mapping.
expires: 2026-09-30
trackingIssue: ARCH-284

Match exceptions against stable architectural identities where possible. A raw line number or generated path can change during harmless refactoring and encourage broad wildcard suppressions. Never allow src/** or an entire rule to be exempted without a separately reviewed policy decision.

The evaluation should fail when an exception expires, when its owner disappears, or when the underlying violation changes shape. It should also report exceptions that no longer match a violation so they can be deleted. A removed violation is progress; leaving its waiver behind creates permission for it to return.

Exceptions need visible cost. Dashboards can show count and age by owner, but raw count is not a universal quality score. One write across a critical data boundary can matter more than twenty low-risk package imports. Review exceptions in the context of the characteristic they weaken and prioritize by consequence.

Use ratchets for measurable migrations. If a repository begins with 37 forbidden imports, store that baseline and require the count never to increase. As changes remove imports, lower the accepted maximum automatically or in reviewed steps. For runtime usage, set dated milestones based on observed traffic and retain rollback capability until the old path reaches zero for a representative window.

Put Ownership and Feedback Near the Change

Every fitness function needs two kinds of ownership. The architecture owner defines why the characteristic matters and approves semantic changes. The implementation owner maintains the analyzer, evidence pipeline, and actionable diagnostics. They may be the same team, but naming both prevents a broken check from being “temporarily” disabled forever.

Run fast deterministic checks in the developer loop and pull requests. Report the rule identifier, violated paths or resources, architectural rationale, permitted remediation, owner, and exception process. A developer should be able to fix a dependency direction without opening a separate architecture document hunt.

Run environment-dependent checks at deployment or on a schedule. A topology evaluator can compare deployed routes, identities, and grants with a versioned manifest. A resilience exercise can run in an isolated or carefully scoped environment. Continuous production observation can verify runtime call direction or ownership assumptions. Feed results into the same catalog even though their cadence differs.

Not every failure should block the same pipeline. A deterministic new cycle can block a pull request. A scheduled resilience exercise may open an incident or improvement item rather than retroactively fail a build. A trend threshold may require review at the next architecture forum. The response should match when evidence becomes available and how quickly risk materializes.

Protect the checks themselves. Pin analyzer versions, test custom rules with positive and negative fixtures, and review changes to ownership manifests and exception files as architecture changes. If a developer can bypass a boundary by renaming a folder, the function is testing convention, not architecture. Prefer compiler-resolved symbols, deployment identities, database grants, and contract schemas over fragile text patterns.

Verify the Fitness Functions Themselves

An automated architecture rule is executable policy and can be wrong. Test it like production code. Give each static rule a minimal fixture that must pass and one that must fail. Include indirect imports, aliases, generated code, test-only dependencies, dynamic entry points, and platform-specific paths where relevant. Mutation testing is conceptually useful: insert a known forbidden edge and prove the pipeline rejects it.

For schema compatibility, retain examples of allowed additive evolution and prohibited removals or semantic changes. For deployment topology, compare the evaluator against a small known manifest and a deliberately misconfigured environment. For resilience functions, verify the fault was actually injected; a “passing” exercise that never severed the dependency is false confidence.

Measure false positives and escapes. A false positive is not just annoyance: repeated noise trains teams to ignore architecture feedback. An escape is a real violation the function missed. Review both after incidents, major refactors, language upgrades, and build-system changes. The response may be to improve evidence, narrow scope, or retire a function whose proxy no longer represents the quality.

Rehearse exception expiry and analyzer outage. Decide whether a check fails closed, fails open with an alert, or blocks only changes in its scope. Critical deterministic boundaries often deserve fail-closed behavior; an unavailable production telemetry backend should not necessarily stop an emergency deployment. Document that policy instead of discovering it under pressure.

Audit outcomes, not only execution. Sample passing changes and ask whether the intended boundary still holds. Compare static graphs with runtime traces where appropriate. An unused permitted dependency may be harmless, while a runtime call constructed through configuration may evade imports entirely. Triangulation turns a convenient check into credible architectural evidence.

Evolve the Architecture and Its Functions Together

Fitness functions are not a constitution carved into the repository. They encode current architectural decisions, and those decisions can change. When a modular monolith deliberately extracts payments, some source dependency rules may become network contract rules, deployment identity checks, and data-access policies. Preserve the protected characteristic while replacing evidence that no longer fits.

Use a lifecycle for every function: proposed, experimental, enforced, deprecated, and retired. Record the decision or risk it protects, introduction date, owner, current threshold, exceptions, and review trigger. Retirement should explain whether the characteristic no longer matters, the architecture changed, or another function supersedes it. Silent deletion erases context.

Watch for gaming. A team can satisfy “no cycles” by moving shared behavior into an unowned utility module, satisfy a service-count rule by building one giant service, or satisfy a call limit by hiding traffic behind a generic gateway. Review the system outcome and revise functions that reward the wrong shape. Multiple complementary checks help, but architectural judgment remains necessary.

There are real tradeoffs. Functions add build time, maintenance, governance, and constraints on local design. Runtime exercises can cost capacity and introduce controlled risk. Overly specific rules calcify implementation; vague metrics produce theater. The remedy is not to abandon automation but to keep the portfolio small, consequential, owned, and open to revision.

Start with one costly failure mode or one important evolution objective. Express it as an observable characteristic, choose direct evidence, run the check without enforcement to learn its behavior, establish a precise baseline, then ratchet toward the target. Add blocking only when the signal is trustworthy and remediation is understood.

Evolutionary architecture is disciplined change, not constant churn. Automated fitness functions provide the feedback loop: they show whether a system still possesses the qualities its owners claim, identify drift near the change that caused it, and make exceptions explicit rather than invisible. When architecture and checks evolve together, teams can move quickly without relying on diagrams to defend themselves.