Architecture Rules & Conventions
These rules form the foundation for how we review pull requests (PRs). While we may occasionally push code that doesn’t strictly follow these rules to production for business reasons, no PR will be merged to the base branch until all rules are met. This is a living proposal: we will continue to iterate and discuss these rules as a team.
Technical Specification: Rules & Conventions
Section titled “Technical Specification: Rules & Conventions”-
UI feedback services (e.g., snackbar service) must only display domain exceptions.
- Only domain-level exceptions should be surfaced to the user; infrastructure or implementation errors should be handled internally.
-
Throwing domain-specific exceptions is only allowed in repositories and services.
- Repositories may throw domain-specific exceptions (e.g., validation, business rule violations).
- Services may throw application-level exceptions (e.g.,
ApplicationExceptionwith user-friendly, potentially localized messages). This is the type of exception consumed by the UI via notifiers. - Note: For simple CRUD flows that bypass the service layer, the repository is responsible for throwing appropriate domain exceptions. See ADR-006.
-
Notifiers should contain minimal logic, only related to UI state.
- For simple CRUD flows, notifiers may directly access repositories. This is not “business logic” - it is data access for presentation.
- For complex business logic (multi-repo orchestration, cross-cutting concerns, multi-step workflows), notifiers must delegate to services.
- Notifiers should focus on state transitions, pagination management, and UI triggers.
- See ADR-006 for criteria on when services are required.
-
UI layer must not contain data cleanup logic (e.g.,
firstName ?? '').- Data cleanup and defaulting should be handled in repositories using model convertors, ensuring domain models are always ready for UI consumption.
-
Features must be independent of one another.
- No feature should directly depend on another; shared logic should be extracted to services or core modules.
-
Use services like PostHog for analytics, feature flags, session replays, and error reporting.
- Centralize all such cross-cutting concerns in dedicated service classes.
-
Caching service:
- All caching logic should be handled by a dedicated caching service, not scattered across repositories or notifiers.
-
Image selection service:
- Image selection and upload logic should be encapsulated in a dedicated service, not in the UI or notifiers.
-
Placeholders for empty, loading, error, and Flutter error states:
- Every feature must provide UI placeholders for these states, with error reporting and optional retry mechanisms.
-
Always use exact dependency versions.
- Avoid caret (
^) or range versions inpubspec.yamlto ensure reproducible builds.
- Avoid caret (
-
List and details are separate features.
- Treat list and detail screens as independent features, even if they share data models.
-
Use
index.dartbarrel files.- Each folder should have an
index.dartto re-export its public API for cleaner imports.
- Each folder should have an
-
Value-focused testing: BDD for business domain, standard unit for technical components.
- Services, complex notifiers, and widget user scenarios use BDD-style Given/When/Then tests. Convertors, CacheHandlerSpecs, and pure functions use standard unit tests. No coverage percentage targets. See Testing Strategy.
-
Use
@riverpodcodegen for all providers.- All Riverpod providers must use the
@riverpodannotation codegen pattern. ManualProvider.autoDisposeis not permitted in new code.
- All Riverpod providers must use the
-
Use the Convertor pattern for all datasource operations.
- The Convertor pattern is the sole datasource pattern. The Strategy/DatasourceModule pattern is retired. See ADR-005.
We write a lot of forms, and while it’s up for research, it’s doubtful that we’ll find a single satisfactory framework for handling all forms. For now, we aim to share consistent behavior across forms:
- All forms must use vanilla Flutter forms.
- Validation may be delegated to an external package if it fits naturally with the Flutter way of handling forms.
- Each form must have its own notifier for managing form data and sharing it with the UI. Following this architecture guideline, form data is usually stored in a
DomainFeatureVarsmodel. - The form data notifier should only handle updates to the form data as made by the user. It should not expose a
submitmethod or handle submission logic. - When the user submits the form, the form UI should directly call the notifier responsible for sending or upserting the data, passing the form data to it. This keeps responsibilities clear and avoids unnecessary coupling between notifiers.
Note: This document is a living specification and will evolve as the architecture and team practices mature.