01

Start with the feedback loop

A linter is excellent at giving immediate, file-local feedback while a developer types. Import plugins can forbid paths, enforce conventions, and often integrate with editor fixes. That speed is a feature, and architecture tests do not replace it.

Architecture tests answer broader questions. They build a dependency graph, select a part of the system, and evaluate relationships, cycles, layers, slices, or metrics across many files. They run as tests because the result represents a system-level invariant, not just one syntax node.

The distinction is less about syntax than about the unit of reasoning. A lint rule usually starts from the file currently being parsed. An architecture rule starts from a project model. Both may inspect an import statement, but only the project model can ask how that edge participates in a cycle, crosses a layer, changes coupling, or violates a repeated module pattern.

02

What linting does exceptionally well

Linting belongs in the tightest feedback loop. It runs while a file is open, points to a source location, and can often suggest or apply a correction. Rules such as restricted imports, naming conventions, dependency allowlists, and forbidden package entry points are valuable precisely because a developer sees them before running the full test suite.

The configuration is also familiar to TypeScript teams. Editors, pre-commit hooks, and CI already know how to execute ESLint. If the architectural decision can be proved from one file and its resolved import, introducing a graph engine may add cost without adding information.

That is why the useful comparison is not which tool is more powerful in the abstract. It is which feedback boundary matches the decision. Fast and local is better when local evidence is sufficient.

  • Prefer linting for immediate file-local import restrictions.
  • Use auto-fixes where the correction is deterministic and safe.
  • Keep style and syntax conventions out of the architecture suite.
  • Let the editor surface failures that do not need a whole-project graph.
03

Where graph context matters

A direct forbidden import is only one architecture failure. A cycle can span several packages. Coupling can rise gradually. A folder selector can become empty after a refactor. A PlantUML model can drift away from the code. These cases need context beyond the file currently open in an editor.

Consider a dependency cycle across orders, billing, and notifications. Every individual import may be allowed, and no single file contains the full failure. The problem appears only when the directed edges are traversed together. The same is true for slice independence and for layer policies that allow one direction while excluding all others.

Metrics also depend on aggregation. Instability compares incoming and outgoing coupling. Cohesion relates methods to the fields they use. Distance from the main sequence combines abstractness and instability. These are not richer forms of linting. They are different questions over a different model.

  • Cycle detection with the complete dependency path
  • Layer and slice rules across a project or monorepo
  • Cohesion, coupling, instability, abstractness, and size metrics
  • Empty-selector protection and structured violations
  • Reports for CI artifacts, reviews, and automation
fitness-functions.ts
import { metrics, projectFiles } from 'archunit';

const cycles = projectFiles()
  .inFolder('src/**')
  .should()
  .haveNoCycles();

const fileSize = metrics()
  .count()
  .linesOfCode()
  .shouldBeBelow(1000);
04

Project resolution is part of correctness

A graph-wide rule is only as correct as the project it extracted. TypeScript path aliases, inherited compiler options, package exports, and project references determine which source node an import reaches. If a tool guesses at those rules or relies on a duplicated configuration, the architecture graph can drift from the build graph.

ArchUnitTS reads the TypeScript configuration and resolves modules through compiler-backed behavior. That costs more than checking one syntax node, but it produces a reusable model aligned with the project that ships. The extracted graph can then support dependency rules, cycles, slices, metrics, and reports without resolving the same repository independently for each check.

This is also why architecture tests normally run after the fastest local checks. They provide broader evidence at a slightly wider feedback boundary, usually a focused test command, pre-push check, or CI job.

05

Operate the two layers as one policy

Teams often duplicate the same boundary in lint configuration and architecture tests without deciding which one is authoritative. That creates two rule languages, two exception mechanisms, and two places that can drift. A clearer operating model assigns each policy to the smallest tool that can prove it, then uses overlap only when the faster feedback is worth the maintenance cost.

For example, a domain package may use a lint restriction to reject direct imports from a database adapter immediately. The architecture suite can protect the broader layer direction, detect transitive cycles, fail empty selectors, and publish a graph artifact. Both checks reinforce the same decision, but each has a distinct responsibility.

Exceptions need the same discipline. Keep them narrow, named, and reviewable. A broad ignore pattern can make either system look green while removing the part of the project that most needs observation.

feedback-boundaries.txt
editor       -> local import restriction -> feedback in seconds
test suite   -> graph-wide invariant      -> feedback before push
CI           -> complete architecture set -> merge gate and artifacts
06

A practical decision guide

Start by writing the decision as a sentence without naming a tool. If it refers to the current file, one import target, or a naming convention, begin with linting. If it refers to a path through the system, a relationship between groups, a cycle, an aggregate metric, or a diagram, use an architecture test.

Then decide how quickly the team needs the answer. A critical local restriction may deserve both an editor rule and a graph-level regression test. A heavier report can stay in CI or a scheduled workflow. Not every check belongs on every keystroke.

Finally, verify the failure mode. Change a path so the selector matches nothing, introduce one known forbidden edge, and confirm that the output names the source and target. A tool choice is only complete when the team has seen how it fails.

07

Use both when both help

The practical answer is often additive. Keep lint rules for fast import feedback and use ArchUnitTS for graph-wide guarantees that belong in the test and delivery pipeline. The architecture rule becomes the durable contract; the linter remains a convenient early warning.

Choose the smallest tool that can prove the decision. If a single import pattern is enough, linting may be enough. If the rule talks about layers, cycles, metrics, diagrams, or a whole project, architecture tests are the clearer home.

This division also scales better organizationally. Product teams retain immediate feedback in the tools they already use, while platform or architecture teams can publish a small set of system-level invariants as reusable test helpers. The result is continuous governance without forcing every design question through a central review meeting.