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
This guide combines patterns from eight rules. Each rule's documentation contains additional examples and the complete configuration reference:
avoid-banned-imports– Control what files can importavoid-banned-exports– Control what files can exportavoid-banned-types– Control type usage by locationbanned-usage– Ban specific methods and constructorsavoid-unsafe-collection-methods– Catch exception-prone collection accessavoid-banned-file-names– Enforce file naming conventionsmax-imports– Limit import count per fileavoid-deprecated-usage– Catch deprecated API usage
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:
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:
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:
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:
class GetUserProfile {
// ❌ LINT: Domain layer must be Flutter-independent.
Color getUserStatusColor(User user) {
return user.isActive ? Colors.green : Colors.red;
}
}
Controlling What Layers Export
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:
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:
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:
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:
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:
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:
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
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.
If your project uses constructor injection outside DI setup, this rule makes that boundary explicit.
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:
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
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
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
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
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.
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.
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.
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.
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.
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:
include: ../../analysis_options.yaml
dcm:
rules:
- max-imports:
max-number: 5 # Domain entities should be simple
include: ../analysis_options.yaml
dcm:
rules:
- max-imports:
max-number: 20 # Tests need more imports for mocks/fixtures
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.
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:
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:
| Pattern | Matches | Does Not Match |
|---|---|---|
lib/.*\.dart | All Dart files in lib | test/test.dart |
lib/domain/.*\.dart | All files in domain folder | lib/data/repo.dart |
lib/(?!domain/).*\.dart | All lib files EXCEPT domain | lib/domain/entity.dart |
| `lib/(?!di/ | injection/).*.dart` | Exclude multiple folders lib/di/setup.dart |
.*_bloc\.dart | Files ending in _bloc.dart | user_cubit .dart |
For detailed pattern explanations, see each rule's documentation.
Complete Configuration
Here's a consolidated configuration implementing all the patterns above:
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.