Skip to main content

Enforcing Architecture Boundaries with DCM

This guide helps you to turn architecture decisions in a Flutter project into enforceable DCM rules. You'll start with a layered app structure, add constraints step by step, and finish with a reusable configuration that protects boundaries in the IDE and in CI.

This guide covers:

  • preventing cross-layer imports and exports
  • restricting framework, state management, and platform-specific APIs by location
  • keeping test code, DI access, and other architectural exceptions out of production code
Rules Covered

This guide combines patterns from eight rules. Each rule's documentation contains additional examples and the complete configuration reference:

Example Project Structure

Use the following layered Flutter project structure as the baseline for the rules in this guide:

lib/
├── domain/ # Business logic, pure Dart
│ ├── entities/
│ │ └── user.dart
│ ├── repositories/ # Abstract interfaces
│ │ └── user_repository.dart
│ └── use_cases/
│ └── get_user_profile.dart
├── data/ # External world: APIs, databases
│ ├── models/
│ │ └── user_dto.dart
│ ├── repositories/
│ │ └── user_repository_impl.dart
│ └── data_sources/
│ └── user_api.dart
├── presentation/ # Flutter UI
│ ├── screens/
│ │ └── profile_screen.dart
│ ├── widgets/
│ │ └── user_avatar.dart
│ └── blocs/
│ └── profile_bloc.dart
├── core/ # Shared utilities
│ └── network/
│ └── api_client.dart
└── di/ # Dependency injection setup
└── injection.dart

The examples below use this structure, but you can adapt the path patterns to your own project.

The next sections show how to enforce the boundaries in this structure with DCM.

Problem 1: Layer Violations

Use this section to prevent layer violations such as using DTOs in widgets or importing Flutter APIs into the domain layer.

Configuration goal: Report imports that break layer boundaries, especially Flutter imports in domain code and cross-layer imports from domain to data or presentation.

Enforcing Import Boundaries

Use avoid-banned-imports to make layer violations impossible:

analysis_options.yaml
dcm:
rules:
- avoid-banned-imports:
entries:
# Domain layer: No Flutter as dependencies whether pure dart or external packages
- paths: ['lib/domain/.*\.dart']
deny:
- 'package:flutter/.*'
- 'lib/data/.*'
- 'lib/presentation/.*'
message: 'Domain layer must not depend on Flutter, the data layer, or the presentation layer.'
severity: error

# Data layer: can use domain, cannot use presentation
- paths: ['lib/data/.*\.dart']
deny:
- 'lib/presentation/.*'
- 'package:flutter/.*'
message: 'Data layer cannot depend on presentation or Flutter.'
severity: error

If a domain file imports Flutter, DCM reports a violation:

lib/domain/entities/user.dart
import 'package:flutter/foundation.dart'; // ❌ LINT: Domain layer must not depend on Flutter, the data layer, or the presentation layer.

class User {
final String id;
final String name;
// ...
}

Once configured, you will see issues both in your IDE and on CI/CD.

Enforcing Type Boundaries

Import rules catch most violations, but not all. Flutter types can still appear through transitive imports or invalid return types in restricted layers.

Use avoid-banned-types to catch these:

analysis_options.yaml
dcm:
rules:
- avoid-banned-types:
entries:
# No Flutter types in domain
- paths: ['lib/domain/.*\.dart']
types:
- '^BuildContext$'
- '^Widget$'
- '^State$'
- '^Color$'
- '^Key$'
message: 'Domain layer must be Flutter-independent.'
severity: error

# No DTOs in presentation, use domain entities
- paths: ['lib/presentation/.*\.dart']
types:
- '.*Dto$'
- '.*Response$'
- '.*Request$'
message: 'Presentation should use domain entities, not DTOs.'
severity: warning

This catches code like:

lib/domain/use_cases/get_user_profile.dart
class GetUserProfile {
// ❌ LINT: Domain layer must be Flutter-independent.
Color getUserStatusColor(User user) {
return user.isActive ? Colors.green : Colors.red;
}
}

Controlling What Layers Export

Why add this rule?

Barrel files such as domain.dart or data.dart define a layer's public API. Restricting what they export helps prevent consumers from depending on implementation details.

Use avoid-banned-exports to keep public APIs clean:

analysis_options.yaml
dcm:
rules:
- avoid-banned-exports:
entries:
# Domain should only export interfaces and entities
- paths: ['lib/domain/.*\.dart']
deny:
- '.*_impl\.dart'
- '.*_dto\.dart'
message: 'Domain should only export interfaces and entities.'
severity: error

# Data layer should not expose internal data sources
- paths: ['lib/data/data\.dart']
deny:
- 'data_sources/.*'
message: 'Do not expose data sources directly. Export repository implementations.'
severity: warning

Problem 2: State Management Chaos

Use this section to enforce a single state management approach across the codebase.

Configuration goal: Report imports, types, and patterns that introduce state management tools outside the chosen standard.

Banning Competing Packages

First, prevent the wrong packages from being imported:

analysis_options.yaml
dcm:
rules:
- avoid-banned-imports:
entries:
- paths: ['lib/.*\.dart']
deny:
- 'package:get/.*'
- 'package:riverpod/.*'
- 'package:flutter_riverpod/.*'
- 'package:provider/provider.dart'
message: 'This project uses BLoC for state management. See docs/architecture.md'
severity: error

Migrating from One Solution to Another

If the codebase is moving from one state management solution to another, start by reporting the old package only in new or migrated areas instead of blocking the entire repository at once.

For example, keep legacy GetX code in lib/legacy/, but block it everywhere else:

analysis_options.yaml
dcm:
rules:
- avoid-banned-imports:
entries:
- paths: ['lib/(?!legacy/).*\.dart']
deny:
- 'package:get/.*'
message: 'New code should use BLoC. Keep GetX only in legacy modules until migration is complete.'
severity: warning

Once the migration is complete, remove the legacy exception and raise the severity to error.

Banning Competing Types

Even without direct imports, types can leak in through dependencies or conditional imports:

analysis_options.yaml
dcm:
rules:
- avoid-banned-types:
entries:
- paths: ['lib/.*\.dart']
types:
- '^GetxController$'
- '^Obx$'
- '^GetBuilder$'
- '^StateNotifier$'
- '^ConsumerWidget$'
- '^ChangeNotifier$'
message: 'Use BLoC/Cubit for state management.'
severity: error

Keeping State Management in Presentation

BLoC belongs in the presentation layer. It shouldn't leak into domain or data:

analysis_options.yaml
dcm:
rules:
- avoid-banned-types:
entries:
- paths: ['lib/domain/.*\.dart', 'lib/data/.*\.dart']
types:
- 'Bloc$'
- 'Cubit$'
- '^BlocProvider$'
- '^BlocBuilder$'
message: 'State management belongs in presentation layer only.'
severity: error

Banning State Management Anti-Patterns

Some patterns technically work but create maintenance nightmares. Use banned-usage to catch them:

analysis_options.yaml
dcm:
rules:
- banned-usage:
entries:
# Ban setState in screen files, use BLoC
- name: setState
description: 'Use BLoC instead of setState in screens. setState is allowed only in self-contained widgets.'
paths: ['lib/presentation/screens/.*\.dart']
severity: warning

# Ban direct emit calls outside bloc files
- type: Bloc
entries:
- name: emit
description: 'Do not call emit() directly. Use event handlers within the BLoC.'
paths: ['lib/(?!.*_bloc\.dart).*\.dart']
severity: error

Problem 3: Scattered API Calls

Use this section to keep HTTP access behind a single abstraction so error handling, authentication, and request behavior stay consistent.

Configuration goal: Report direct HTTP usage outside the shared network abstraction.

Banning Direct HTTP Usage

analysis_options.yaml
dcm:
rules:
- banned-usage:
entries:
# Ban direct Dio method calls outside the network layer
- type: Dio
entries:
- name: get
description: 'Use ApiClient.get() for consistent error handling and auth.'
- name: post
description: 'Use ApiClient.post() for consistent error handling and auth.'
- name: put
description: 'Use ApiClient.put() for consistent error handling and auth.'
- name: delete
description: 'Use ApiClient.delete() for consistent error handling and auth.'
paths: ['lib/(?!core/network/).*\.dart']
severity: error

# Ban http package entirely
- name: http.get
description: 'Use ApiClient instead of the http package.'
severity: error

- name: http.post
description: 'Use ApiClient instead of the http package.'
severity: error

Problem 4: Restricting DI Container Access

Use this section to allow DI container access only in di/ and report it everywhere else.

Configuration goal: Report DI container types and service locator calls outside di/ or your DI setup.

Why add this rule?

If your project uses constructor injection outside DI setup, this rule makes that boundary explicit.

analysis_options.yaml
dcm:
rules:
# Ban GetIt type outside DI files
- avoid-banned-types:
entries:
- paths: ['lib/(?!di/).*\.dart']
types:
- '^GetIt$'
message: 'Access GetIt only in DI configuration. Use constructor injection elsewhere.'
severity: error

# Ban service locator calls
- banned-usage:
entries:
- name: GetIt.I
description: 'Use constructor injection instead of service locator pattern.'
paths: ['lib/(?!di/).*\.dart']
severity: error

- name: GetIt.instance
description: 'Use constructor injection instead of service locator pattern.'
paths: ['lib/(?!di/).*\.dart']
severity: error

Problem 5: Test Code in Production

Use this section to keep test-only code out of production code.

Configuration goal: Report test files, test imports, and test-only types when they appear in production code.

Banning Test File Names in Production

Use avoid-banned-file-names to catch misplaced test files:

analysis_options.yaml
dcm:
rules:
- avoid-banned-file-names:
entries:
- 'lib/.*_test\.dart$'
- 'lib/.*_mock\.dart$'
- 'lib/.*_fake\.dart$'
- 'lib/.*_stub\.dart$'
- 'lib/.*_fixture\.dart$'

Banning Test Imports in Production

analysis_options.yaml
dcm:
rules:
- avoid-banned-imports:
entries:
- paths: ['lib/.*\.dart']
deny:
- 'package:test/.*'
- 'package:flutter_test/.*'
- 'package:mockito/.*'
- 'package:mocktail/.*'
- 'package:fake_async/.*'
message: 'Test packages cannot be imported in production code.'
severity: error

Banning Test Types in Production

analysis_options.yaml
dcm:
rules:
- avoid-banned-types:
entries:
- paths: ['lib/.*\.dart']
types:
- '^Mock'
- 'Mock$'
- '^Fake'
- 'Fake$'
- '^Stub'
- '^WidgetTester$'
message: 'Mock/Fake/Stub types are only allowed in tests.'
severity: error

Problem 6: Platform Code Leaks

Use this section to keep platform-specific APIs out of shared code.

Configuration goal: Report platform-specific imports and types when they appear in incompatible shared or target-specific files.

Banning Platform-Specific Imports

analysis_options.yaml
dcm:
rules:
- avoid-banned-imports:
entries:
# Ban dart:io in web-targeted code
- paths: ['lib/.*_web\.dart', 'lib/.*web.*/.*\.dart']
deny:
- 'dart:io'
- 'dart:ffi'
- 'package:path_provider/.*'
message: 'dart:io is not available on web. Use platform abstraction.'
severity: error

# Ban dart:html in mobile-targeted code
- paths: ['lib/(?!.*_web\.dart|.*web.*).*\.dart']
deny:
- 'dart:html'
- 'dart:js'
- 'package:js/.*'
message: 'dart:html is only available on web.'
severity: error

Banning Platform-Specific Types

analysis_options.yaml
dcm:
rules:
- avoid-banned-types:
entries:
# Ban IO types in web code
- paths: ['lib/.*_web\.dart']
types:
- '^File$'
- '^Directory$'
- '^Process$'
- '^Socket$'
message: 'IO types are not available on web. Use platform abstraction.'
severity: error

Problem 7: Design System Bypass

Use this section to report raw Flutter widgets in presentation code and allow them only inside the design system.

Configuration goal: Report direct usage of framework widgets that should be replaced by design system components.

analysis_options.yaml
dcm:
rules:
- banned-usage:
entries:
# Ban raw button constructors
- type: ElevatedButton
entries:
- name: new
description: 'Use DSButton.primary() or DSButton.secondary() from design system.'
paths: ['lib/presentation/.*\.dart']
exclude-paths: ['lib/design_system/.*']
severity: error

- type: TextButton
entries:
- name: new
description: 'Use DSButton.text() from design system.'
paths: ['lib/presentation/.*\.dart']
exclude-paths: ['lib/design_system/.*']
severity: error

# Ban raw input constructors
- type: TextField
entries:
- name: new
description: 'Use DSTextField for consistent styling and validation.'
paths: ['lib/presentation/.*\.dart']
exclude-paths: ['lib/design_system/.*']
severity: error

# Ban raw image loading
- type: Image
entries:
- name: network
description: 'Use DSImage.network() for caching and error handling.'
paths: ['lib/presentation/.*\.dart']
exclude-paths: ['lib/design_system/.*']
severity: warning

Problem 8: Inconsistent Wrappers

Use this section to report direct calls to framework APIs that should go through project wrappers.

Configuration goal: Report raw navigation, dialog, time, and logging APIs when project wrappers should be used instead.

analysis_options.yaml
dcm:
rules:
- banned-usage:
entries:
# Navigation
- name: Navigator.push
description: 'Use AppRouter.push() for analytics tracking.'
severity: warning

- name: Navigator.pushNamed
description: 'Use AppRouter.pushNamed() for deep link support.'
severity: warning

# Dialogs
- name: showDialog
description: 'Use DialogService.show() for consistent styling.'
severity: warning

- name: showModalBottomSheet
description: 'Use BottomSheetService.show() for consistent styling.'
severity: warning

# Time
- name: DateTime.now
description: 'Use Clock.now() for testability. Inject Clock via constructor.'
severity: error

# Logging
- name: print
description: 'Use Logger.d() / Logger.e() for structured logging.'
severity: error

- name: debugPrint
description: 'Use Logger.d() for structured logging.'
severity: warning

Problem 9: Dangerous APIs

Use this section to report exception-prone APIs and guide developers toward safer alternatives.

Configuration goal: Flag unsafe collection access and guide developers toward null-safe alternatives.

analysis_options.yaml
dcm:
rules:
- avoid-unsafe-collection-methods:
severity: warning

This dedicated rule covers unsafe access such as first, last, single, firstWhere, lastWhere, singleWhere, and [] on iterables. You may also use banned-usage however, we recommend avoid-unsafe-collection-methods over banned-usage for collection-safety checks because it is purpose-built and provides clearer guidance.

Problem 10: File Naming Chaos

Use this section to enforce consistent file naming and block generic dumping-ground files.

Configuration goal: Report file names that break snake_case or rely on overly generic names.

analysis_options.yaml
dcm:
rules:
- avoid-banned-file-names:
entries:
# Enforce snake_case (ban uppercase letters)
- '[A-Z]'
# Ban generic dumping-ground files
- '/utils\.dart$'
- '/helpers\.dart$'
- '/constants\.dart$'
- '/common\.dart$'
- '/misc\.dart$'
# Ban backup/temp files
- '\.bak$'
- '\.tmp$'
- '_copy\.dart$'

Problem 11: Import Complexity

Use this section to limit import count and surface files that are taking on too many responsibilities.

Configuration goal: Report files whose import count exceeds the limit for their layer or purpose.

analysis_options.yaml
dcm:
rules:
- max-imports:
max-number: 12 # Default for most files

If you still want stricter limits in specific layers, you can use nested analysis_options.yaml files:

lib/domain/analysis_options.yaml
include: ../../analysis_options.yaml

dcm:
rules:
- max-imports:
max-number: 5 # Domain entities should be simple
test/analysis_options.yaml
include: ../analysis_options.yaml

dcm:
rules:
- max-imports:
max-number: 20 # Tests need more imports for mocks/fixtures
info

Nested analysis_options are possible, but not recommended. We are working on bringing entries support for this rule and will keep this section up to date once that is released.

Problem 12: Migration Tracking

Use this section to surface deprecated APIs during a migration and raise enforcement over time.

Configuration goal: Start by reporting deprecated APIs as warnings, then promote them to errors when the migration is ready to block CI.

analysis_options.yaml
dcm:
rules:
# Phase 1: Surface deprecated usage as warnings
- avoid-deprecated-usage:
severity: warning

# Optionally, be explicit about specific deprecations
- banned-usage:
entries:
- name: OldApiClient.fetch
description: 'Deprecated: Migrate to NewApiClient.request() by Q2.'
severity: warning

- name: LegacyAuth.login
description: 'Deprecated: Use AuthService.signIn() with OAuth flow.'
severity: warning

When ready to enforce:

analysis_options.yaml
dcm:
rules:
- avoid-deprecated-usage:
severity: error # Now blocks CI

Understanding Path Patterns

Most rules use regex patterns for paths and exclude-paths. Here's a quick reference:

PatternMatchesDoes Not Match
lib/.*\.dartAll Dart files in libtest/test.dart
lib/domain/.*\.dartAll files in domain folderlib/data/repo.dart
lib/(?!domain/).*\.dartAll lib files EXCEPT domainlib/domain/entity.dart
`lib/(?!di/injection/).*.dart`Exclude multiple folders lib/di/setup.dart
.*_bloc\.dartFiles ending in _bloc.dartuser_cubit .dart

For detailed pattern explanations, see each rule's documentation.

Complete Configuration

Here's a consolidated configuration implementing all the patterns above:

analysis_options.yaml
dcm:
rules:
# ═══════════════════════════════════════════════════════════════
# LAYER BOUNDARIES
# ═══════════════════════════════════════════════════════════════

- avoid-banned-imports:
entries:
# Domain: pure Dart
- paths: ['lib/domain/.*\.dart']
deny:
- 'package:flutter/.*'
- 'lib/data/.*'
- 'lib/presentation/.*'
message: 'Domain layer must be pure Dart.'
severity: error
# Data: no presentation
- paths: ['lib/data/.*\.dart']
deny:
- 'lib/presentation/.*'
message: 'Data layer cannot depend on presentation.'
severity: error
# State management standardization
- paths: ['lib/.*\.dart']
deny:
- 'package:get/.*'
- 'package:riverpod/.*'
message: 'Use flutter_bloc for state management.'
severity: error
# Test packages in production
- paths: ['lib/.*\.dart']
deny:
- 'package:test/.*'
- 'package:flutter_test/.*'
- 'package:mockito/.*'
message: 'Test packages not allowed in production.'
severity: error
# Platform safety
- paths: ['lib/.*_web\.dart']
deny:
- 'dart:io'
message: 'dart:io unavailable on web.'
severity: error

- avoid-banned-types:
entries:
# Domain: no Flutter
- paths: ['lib/domain/.*\.dart']
types:
- '^BuildContext$'
- '^Widget$'
- '^Color$'
message: 'Domain must be Flutter-independent.'
severity: error
# State management types
- paths: ['lib/.*\.dart']
types:
- '^GetxController$'
- '^StateNotifier$'
message: 'Use BLoC for state management.'
severity: error
# DI only in di/
- paths: ['lib/(?!di/).*\.dart']
types:
- '^GetIt$'
message: 'Use constructor injection.'
severity: error
# Test types in production
- paths: ['lib/.*\.dart']
types:
- '^Mock'
- '^Fake'
message: 'Test types not allowed in production.'
severity: error

- avoid-banned-exports:
entries:
- paths: ['lib/domain/.*\.dart']
deny:
- '.*_impl\.dart'
message: 'Export interfaces only from domain.'
severity: error

- avoid-banned-file-names:
entries:
- '[A-Z]'
- 'lib/.*_test\.dart$'
- 'lib/.*_mock\.dart$'
- '/utils\.dart$'

- banned-usage:
entries:
# Custom wrappers
- name: Navigator.push
description: 'Use AppRouter.push() for analytics.'
severity: warning
- name: showDialog
description: 'Use DialogService.show() for consistency.'
severity: warning
- name: DateTime.now
description: 'Use Clock.now() for testability.'
severity: error
- name: print
description: 'Use Logger for structured logging.'
severity: error
# Design system
- type: ElevatedButton
entries:
- name: new
description: 'Use DSButton from design system.'
paths: ['lib/presentation/.*\.dart']
exclude-paths: ['lib/design_system/.*']
severity: error

- avoid-unsafe-collection-methods:
severity: warning

# ═══════════════════════════════════════════════════════════════
# COMPLEXITY & DEPRECATION
# ═══════════════════════════════════════════════════════════════

- max-imports:
max-number: 12

- avoid-deprecated-usage:
severity: warning

If you need a configuration pattern not covered here, let us know via Email or on Discord.