DEV Community

Cover image for Stop your Flutter architecture from drifting: layer rules and cycle checks in your IDE and CI with arch_guard
Piyush Anand
Piyush Anand

Posted on AI-assisted

Stop your Flutter architecture from drifting: layer rules and cycle checks in your IDE and CI with arch_guard

Every Flutter project I've worked on started with a clean folder structure: domain, data, presentation, maybe a features/ folder. And on every one, six months later, a widget was importing a repository implementation directly, and two services imported each other.

Nobody did it on purpose. A deadline, a quick fix, a review that missed one import line. The analyzer doesn't care, the tests pass, and the architecture diagram in the wiki is now fiction.

So I built arch_guard: a CLI, Dart library and analyzer plugin that reads every import, export and part in your project and tells you when the code no longer matches the architecture you designed: in the editor as you type, in dart test, and in CI.

Interactive dependency graph

Try it in 30 seconds

dart pub add --dev arch_guard
dart run arch_guard init     # detects your layout and writes arch_guard.yaml
dart run arch_guard          # checks the project
Enter fullscreen mode Exit fullscreen mode

init looks at your folders and picks a preset: Clean Architecture, Riverpod, Bloc or feature-first. It then previews the first scan: how many files landed in each layer, what's already broken, and what to do next.

What it checks

1. Layer rules

The simplest config is one line:

# arch_guard.yaml
preset: clean_architecture
Enter fullscreen mode Exit fullscreen mode

That means domain may only import core, and presentation may not import data. When something breaks a rule, you get the file, the line, and the rule:

❌ Layer Violation: [presentation] lib/features/auth/presentation/page.dart:3 -> [data] lib/features/auth/data/repo.dart
   Rule: `presentation` may only import `core`, `domain`, `presentation`.
Enter fullscreen mode Exit fullscreen mode

The other presets:

  • riverpod: domain imports nothing else; data never imports application or presentation.
  • bloc: UI → bloc/cubit → repository → data provider, with models usable everywhere.
  • feature_first: each folder in lib/features becomes its own layer, and a feature may import only itself and shared code. This is the one I find catches the most: features quietly reaching into each other.

Presets are a starting point. Override just the part you disagree with:

preset: clean_architecture
layers:
  presentation:
    allowed_imports: [core, domain, presentation, data]   # we allow UI -> data here
Enter fullscreen mode Exit fullscreen mode

2. Forbidden packages

Some rules aren't about your own folders. "The domain layer must not know about Flutter" is a package rule:

forbidden_packages: [dio]            # nowhere in the project
layers:
  domain:
    forbidden_packages: [flutter, "dart:io", "firebase_*"]
Enter fullscreen mode Exit fullscreen mode

3. Circular dependencies, with a fix

arch_guard runs Tarjan's algorithm over the import graph and reports every group of files that import each other, with fan-in/fan-out, instability and the "hub" files. Each cycle also names the one import to remove:

Strongly Connected Component #1 (2 files)
  Representative Cycle Path:
  lib/widgets/nav.dart
  -> lib/screens/home.dart
  -> lib/widgets/nav.dart
  Suggested fix: remove lib/screens/home.dart:1 -> lib/widgets/nav.dart (breaks the cycle)
Enter fullscreen mode Exit fullscreen mode

max_scc_size: 5 fails the run if a cycle group grows beyond 5 files, and dart run arch_guard --explain <file> shows why a file is in one.

4. Coupling limits and monorepos

  • metrics: { max_fan_out: 20, max_fan_in: 30 } stops files from turning into hubs; reports list the most coupled files.
  • In a Dart workspace or Melos-style packages/ + apps/ repo, workspace_rules say which packages may depend on which (apps/* → packages/*, never the reverse), and cycles across package boundaries are found too.

5. A graph you can actually look at

dart run arch_guard -f html -o build/reports
Enter fullscreen mode Exit fullscreen mode

gives you an interactive graph that works offline. There's also Mermaid (paste it into a PR or README), Graphviz DOT and JSON.

In your editor

CI catching a bad import is good; never writing it is better. The companion
arch_guard_lint analyzer plugin reads the same arch_guard.yaml and underlines the import in VS Code, Android Studio and dart analyze:

# analysis_options.yaml (Dart 3.10 / Flutter 3.38+)
plugins:
  arch_guard_lint: ^0.2.0
Enter fullscreen mode Exit fullscreen mode

[IDE clip: underlined import → quick fix]

  • layer_violation and forbidden_package on the offending import.
  • circular_dependency on the one import that closes each cycle, updated as you type, including unsaved changes.
  • Quick fixes: Remove this import, or Allow layer X to import Y, which edits allowed_imports in your config for you. There's deliberately no "allow" fix for forbidden packages: loosening a "never" rule should be a conscious edit.

In your tests

If you'd rather keep architecture rules next to the code, they also run in dart test / flutter test:

import 'package:arch_guard/testing.dart';
import 'package:test/test.dart';

void main() {
  test('architecture', () async {
    await expectArchitecture();
  });
}
Enter fullscreen mode Exit fullscreen mode

It fails exactly when the CLI would, with a readable list of problems.

Adopting it on a codebase that's already messy

This was the part I cared most about. If a tool reports 200 violations on day one, it gets uninstalled on day one.

dart run arch_guard --update-baseline
git add arch_guard_baseline.json
Enter fullscreen mode Exit fullscreen mode

That records today's problems. From then on, CI fails only on new cycles and violations (the IDE plugin stays quiet about baselined ones too). When someone fixes an old one, arch_guard tells you, so you can tighten the baseline and stop it from coming back.

For big PRs on big apps, --since origin/main reports only problems touching files changed on the branch, while still analysing the whole project.

Running it on every pull request

# .github/workflows/architecture.yml
name: Architecture
on: [pull_request]
permissions:
  contents: read
  pull-requests: write
jobs:
  arch_guard:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: creatorpiyush/arch_guard@v1.8.0
Enter fullscreen mode Exit fullscreen mode

The action downloads a checksum-verified binary (no Dart SDK setup), keeps a single PR comment up to date, and can upload SARIF so violations show up in GitHub Code Scanning. There's also a pre-commit hook and recipes for lefthook, plain git hooks and GitLab CI.

How it compares

  • DCM is a full linting suite with hundreds of rules and IDE integration; its free tier covers one seat up to 50k LOC. If you already use it, its banned-imports rule covers part of this.
  • import_lint shows import rules right in your editor as an analyzer plugin.
  • lakos focuses on graph metrics and DOT/JSON output.

arch_guard does one job: make architecture rules easy to adopt (presets, init, baselines) and enforce them everywhere you work: editor, tests and every PR.

Links

It's MIT-licensed. If your project uses a layout none of the presets fit, I'd really like to hear about it: open an issue or leave a comment with your folder structure, and I'll look at adding a preset.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.