This content originally appeared on DEV Community and was authored by Anton Malofeev
Thoughts about scalable code structure based on developer as user philosophy combined with iterative design using AI.
Dev Architecture Series
Part 1 — Intro Notes
Part 2 — Stage 1 and 2, Previous
Part 3 — Stage 3,4,5 and Conclusion – You are here:)
Part 4 — Libraries as Agentic Executables (planned)
Part 5 — Maintaining Libraries with AI Agents (planned)
Part 6 — Building Foundation with Open Source in mind (planned)
Part 7 — AI Project Bootstrapping (planned)
Part 8 — Storage Layer as Reliability Foundation (planned)
Part 9 — How to build for different Stores? (planned)
Part 10 — Dev Ethics as Dev Cornerstone (Decision Making) (planned)
Part 11 — Domain Knowledge (Agent Domain Context) (planned)
Part 12 — Building Tests with AI Agents (planned)
Introduction
In the first part of this series, we defined key terms and principles. In Part 2, we walked through Stage 1 and Stage 2. If you missed them, check out the Part 1 — Intro Notes and Part 2 — Stage 1 and 2
Note: after using Cursor AI and other different agents for about a year it’s become obvious that some changes are needed, therefore I will continue architecture series in future (so the content become a plan xD) — if you have any thoughts — please leave comment:).
Let’s continue our journey!
Stage 3 — Feature Iterations
Team: 2 Developers
Main focus: Test and try features for specific domains to make these domains logically complete.
While this part is quite small for changes — and may not contain any radical changes in structure, I think it’s quite important on this stage to follow two principles:
Keep refreshing, refining, and updating domain knowledge from testing and using the application.
Keep every small feature implementation separate from the screen where it is placed. Treat the screen as a layout where you place widgets, and it would be much easier to reuse any feature-related widgets, as they will behave as self-contained micro applications.
Stage 4 — Scaling
Team: 2–3 Developers
In my opinion, there are two paths at this stage:
Path 1 — Scaling to Other Platforms
This stage means that we need to publish it to stores on several platforms simultaneously.
In that case, we will have to deal not only with different factors: platform, adaptive, responsive factors (https://docs.flutter.dev/ui/adaptive-responsive/best-practices)
That means, it would be better to make the following changes to the structure.
First, move the whole data layer (folders with prefixes data) and core into a new package “core”.
/packages/
├── core/ // or data layer
│ ├── core.dart
│ ├── src/
│ │ ├── core/ // I prefer to keep core as a separate package, as it often has tendency to be reused in other projects, and this way it would be easier to separate it in future to a separate package.
│ │ │ ├── utils/
│ │ │ ├── extensions/
│ │ │ ├── hooks/
│ │ │ ├── l10n/
│ │ │ └── side_services/
│ │ ├── data_local_api/
│ │ ├── data_remote_api/
│ │ ├── data_repositories/
│ │ ├── data_models/
│ │ │ ├── api_dto/
│ │ │ └── {domain}_models/
│ │ ├── data_states/
│ │ ├── di/
Second, move the whole ui_kit into a separate package.
/packages/
├── ui_kit/
│ ├── ui_kit.dart
│ ├── UI_KIT_README.md
│ ├── atoms/
│ │ ├── ATOM_README.md
│ ├── molecules/
│ │ ├── MOLECULES_README.md
Third, take a look at domains and features and where they are placed.
What you can find is that if most of the features were separated from screens inside flows, it is quite easy to separate every screen to the following functionality:
- View Layouts // <- layout where features were placed.
- Flow logic // <- navigation between views
- Features // <- business implementation
Then, move all domain-related logic — layouts (optional), flow (optional), features to a new package “ui_domains”.
This will be your foundation, where all features, sharable layouts, and flows for domains are written. Think of it like a box of blocks, which separately have some independent functionality within a scope of a specific domain.
Because every platform has its own layouts, flows can be different. For example, where in mobile you will push several screens, in desktop you may open just one window.
Then, depending on your end goal, create a separate app for every platform you need.
For example, you can end up with the following structure:
Abstract code structure
/
/apps/
├── {platform}_app/ // <- separate applications with new entry main, which can use different layouts, based on ui_domains package.
/packages/
│ ├── core/
│ ├── ui_kit/
│ ├── ui_domains/ // <- this new package will be used in all apps
│ │ ├── src/
│ │ │ ├── ui_{domain}/
│ │ │ │ ├── {DOMAIN}_KNOWLEDGE.md
│ │ │ │ ├── {flow}/
│ │ │ │ │ ├── {feature_name}/
│ │ │ │ │ ├── ├── {feature_name}.dart // barrel file with conditional exports, which are extremly useful for platform dependent features https://dart.dev/tools/pub/create-packages#conditionally-importing-and-exporting-library-files
│ │ │ │ │ ├── ├── {feature_name}_{platform}/
│ │ │ │ │ ├── layouts/ // <- typically you may have different layouts for same features, so it is make sense to place it directly in the flow folder. But in same time, if you need to move to different place - thats fine too - since in my opinion, that's more business/design related decision.
Example code structure
/
/apps/
├── desktop_app/
├── web_app/
├── mobile_app/
/packages/
├── core/
├── ui_kit/
├── ui_domains/
│ ├── src/
│ │ ├── ui_payments/ // <- domain
│ │ │ ├── ui_payments.dart // <- barrel file with exports
│ │ │ ├── PAYMENTS_KNOWLEDGE.md // <- domain knowledge
│ │ │ ├── pay/ // <- flow
│ │ │ │ ├── qr_pay/ // <- feature
│ │ │ │ │ ├── qr_pay_feature.dart // <- feature barrel file with conditional imports
│ │ │ │ │ ├── qr_pay_ios/ // <- platform feature implementation
│ │ │ │ │ ├── qr_pay_android/
│ │ │ │ │ ├── qr_pay_web/
│ │ │ │ ├── provider_pay/ // <- feature
│ │ │ │ │ ├── provider_pay.dart // <- feature barrel file
│ │ │ │ │ layouts/ // <- flow layouts
│ │ │ │ │ ├── desktop/
│ │ │ │ │ ├── mobile/
│ │ │ ui_profile/ // <- domain
│ │ │ ├── ui_profile.dart // <- domain barrel file
│ │ │ ├── PROFILE_KNOWLEDGE.md // <- domain knowledge
│ │ │ ├── auth/ // <- flow, but as you can imagine, if it will outgrow just auth logical domain, the transform it into a domain and move up.
│ │ │ │ ├── login/ // <- feature
│ │ │ │ ├── ├── login_ios/
│ │ │ │ ├── ├── login_android/
│ │ │ │ ├── ├── login_web/
│ │ │ │ ├── registration/
│ │ │ │ ├── ├── registration_ios/
│ │ │ │ ├── ├── registration_android/
│ │ │ │ ├── ├── registration_web/
│ │ │ │ │ layouts/ // <- flow layouts
│ │ │ │ │ ├── desktop/
│ │ │ │ │ ├── mobile/
│ │ │ ├── settings/ // <- flow
│ │ │ │ ├── settings.dart // <- flow barrel file
│ │ │ │ ├── customization/ // <- feature
│ │ │ │ ├── ├── customization_ios/
│ │ │ │ ├── ├── customization_android/
│ │ │ │ ├── ├── customization_web/
The difference will depend on what features are represented. For example, some iOS native functionality may not just work or not exist on Linux desktop. Therefore, you will need create a Linux implementation specific to a feature or construct an entirely different experience using only some of iOS features and separate it into independent package.
The great documentation how to write native plugins and packages is described in flutter website (https://docs.flutter.dev/packages-and-plugins/developing-packages).
Path 2 — Same App, Different Configurations
Another way to scale your application by having different flavors. https://docs.flutter.dev/deployment/flavors . But at the same time, if the differences are huge, it would be reasonable to reconstruct them as different apps.
So in contrast with Path 1, I think in this path you may have the same layouts, but new domains, new flows, and new specific to new apps features. The important part, that you may or may not include domains, flows or features to different apps.
Abstract code structure
/
/apps/{mobile_app_with_different_flows}/
/packages/
│ ├── core/
│ ├── ui_kit/
│ ├── ui_domains/
│ │ ├── src/
│ │ │ ├── ui_{domain}/
│ │ │ │ ├── {DOMAIN}_KNOWLEDGE.md
│ │ │ │ ├── {flow}/
│ │ │ │ │ ├── {feature_name}/
│ │ │ │ │ ├── {experimental_feature_name}/
│ │ │ │ ├── {experimental_flows}/
│ │ │ │ │ ├── {feature_name}/
│ │ │ ├── ui_{experimental_domain}/
│ │ │ │ ├── {DOMAIN}_KNOWLEDGE.md
│ │ │ │ ├── {flow}/
│ │ │ │ │ ├── {feature_name}/
│ │ │ │ │ ├── ├── {feature_name}_{platform}/
Example code structure
/
/apps/
├── mobile_app_spring_edition // <- separate applications with new entry main, which can use different layouts, based on ui_domains package.
├── mobile_app_with_gamification/
├── mobile_app_with_ai/
/packages/
├── core/
├── ui_kit/
├── ui_domains/
│ ├── src/
│ │ ├── ui_payments/
│ │ │ ├── ui_payments.dart
│ │ │ ├── PAYMENTS_KNOWLEDGE.md
│ │ │ ├── pay/ // <- flow
│ │ │ │ ├── galactical_pay/ // <- experimental feature
│ │ │ ├── scan_and_pay/ // <- experimental flow
│ │ │ ui_ai_chatbot/ // <- experimental domain
│ │ │ ├── ui_ai_chatbot.dart
│ │ │ ├── AI_CHATBOT_KNOWLEDGE.md
│ │ │ ui_waiting_game/
│ │ │ ├── ui_waiting_game.dart
│ │ │ ├── WAITING_GAME_KNOWLEDGE.md
│ │ │ ui_picture_board/
│ │ │ ├── ui_picture_board.dart
│ │ │ ├── PICTURE_BOARD_KNOWLEDGE.md
Stage 5 — Scaling Application Domains
Team: 3–6 Developers
Lastly, let’s talk about what you can do if a feature grows too big, and you have enough resources to maintain it independently.
Since from the start, you have separated the domains clearly, you have several options:
- Extract the domain as a package
- Extract specific flows as a package
- Extract specific features as a package
Abstract code structure:
/packages/
├── core/
├── ui_kit/
├── ui_{domain}/
│ ├── {DOMAIN}_KNOWLEDGE.md
│ ├── src/
│ │ ├── {flow}/
│ │ │ ├── {feature_name}/
│ │ │ │ ├── {feature_name}_{platform}/
│ │ │ ├── layouts/
│ │ │ │ ├── {layout_name}/
Example — moving ui_payments domain to packages:
/packages/
├── ui_payments/
│ ├── ui_payments.dart
│ ├── README.md
│ ├── PAYMENTS_KNOWLEDGE.md
│ ├── src/
│ │ ├── provider_pay/ // <- flow
│ │ │ ├── provider_pay.dart // <- feature
│ │ │ ├── ├── provider_pay_ios/ // <- platform feature implementation
│ │ │ ├── ├── provider_pay_android/
│ │ │ ├── ├── provider_pay_web/
│ │ │ │ layouts/
│ │ │ │ ├── desktop/
│ │ │ │ ├── mobile/
│ │ │ pay/ // <- flow
│ │ │ ├── qr_pay/ // <- feature
│ │ │ │ ├── qr_pay_feature.dart // <- feature
│ │ │ │ ├── ├── qr_pay_ios/ // <- platform feature implementation
│ │ │ │ ├── ├── qr_pay_android/
│ │ │ │ ├── ├── qr_pay_web/
│ │ │ │ layouts/
│ │ │ │ ├── desktop/
│ │ │ │ ├── mobile/
In any case, since on the previous stage you already have an independent data layer as a package + an independent ui_kit, your task is quite simple — treat all and plug them as APIs, and use them the same you use any Flutter widget or any pub package.
This way, you will still have non-broken dependencies (remember the combination: barrel files + clear and visible separated boundaries between data layers and ui, domains, flows and features), while maintaining flexibility to scale, research, and develop. Hooray!
Conclusion
In my opinion, if you are an indie developer, you can start small and grow. For a team with several developers, a designer, and analyst, and especially if you have a predefined design, it would be better to start from later stages, as you already have more resources at hand and can handle larger complexity.
But, also, I think this approach works both ways. For example, if your business is in crisis mode and your developer team is shrinking, basically that will mean that you do not have enough resources to keep huge infrastructure. In that case, in my opinion, it would be extremely helpful to go gently back to early stages and continue from there — that may save you from burnout. And don’t forget about your ideas in prototypes — maybe they will save your team one day:)
I hope this approach will give you inspiration to iterate, integrate, or to look at your projects from different perspectives and to give more quality to the end user.
If you found this useful, please share and leave your thoughts in the comments, and have an amazing journey ahead!
Thoughts about scalable code structure based on developer as user philosophy combined with iterative design using AI.
Dev Architecture Series
Part 1 — Intro Notes
Part 2 — Stage 1 and 2, Previous
Part 3 — Stage 3,4,5 and Conclusion – You are here:)
Part 4 — Libraries as Agentic Executables (planned)
Part 5 — Maintaining Libraries with AI Agents (planned)
Part 6 — Building Foundation with Open Source in mind (planned)
Part 7 — AI Project Bootstrapping (planned)
Part 8 — Storage Layer as Reliability Foundation (planned)
Part 9 — How to build for different Stores? (planned)
Part 10 — Dev Ethics as Dev Cornerstone (Decision Making) (planned)
Part 11 — Domain Knowledge (Agent Domain Context) (planned)
Part 12 — Building Tests with AI Agents (planned)
This content originally appeared on DEV Community and was authored by Anton Malofeev