Flutter apps need structure that is easy to orient yourself in, testable and maintainable.
With a client-centric service such as Firebase Firestore, it's extremely important to keep your code clean.
Domain-Driven Design - the big picture
We can use a view model of our choice, whether it be ChangeNotifier, MobX Store, or even the new StateNotifier.
The domain layer is completely independent of all the other layers. Just pure business logic & data.
With Domain-Driven Design, UI is dumbest part of the app. That's because it's at the boundary of our code and it's totally dependent on the Flutter framework.
Architectural Layers of DDD
Unlike with the spaghetti architecture ๐, you will always know where to put a certain class when you're following the Domain-Driven Design principles.
Every architectural layer contains features and possibly a core folder which holds classes common to all the features in that layer (helpers, abstract classes, ...).
For folder structure Layers will hold features, not the other way around. This will still keep the code readable but, most importantly, it will ensure that adding more features and sub-features is going to be a pure bliss.
Presentation Layer
- This layer is all Widgets and also the state of the Widgets.
- Its logic is limited to creating "eye candy" for the user.
- A rule of thumb is that whenever some logic operates with data that is later on sent to a server or persisted in a local database, that logic has nothing to do in the presentation layer. So while animation code does belong into this layer, even things like form validation are NOT done inside the presentation layer.
Application Layer
- Application layer has only one job - orchestrating all of the other layers.
- No matter where the data originates (user input, real-time Firestore Stream, device location), its first destination is going to be the application layer.
- You aren't going to find any UI code, network code, or database code here. The role of the application layer is to decide "what to do next" with the data.
- It doesn't perform any complex business logic, instead, it mostly just makes sure that the user input is validated (by calling things in the domain layer) or it manages subscriptions to infrastructure data Streams (not directly, but by utilizing the dependency inversion principle).
Domain Layer
- The domain layer is the pristine center of an app.
- It is fully self contained and it doesn't depend on any other layers and is not concerned with anything but doing its own job well.
- This is the part of an app which doesn't care if you switch from Firebase to a REST API or if you change your mind and you migrate from the Hive database to Moor.
- All the other layers do depend on domain.
- This is where your business logic lives.
- Everything which is not Flutter/server/device dependent goes into domain.
- The domain layer is also the home of Failures.
- In addition to holding and carrying around data, Entities and validated ValueObjects it also contain logic. This ranges from data validation and helpers to complex computations.
The domain layer is the core of you app. Changes in the other layers don't affect it. However, changes to the domain affect every other layer.
Infrastructure Layer
- Much like presentation, this layer is also at the boundary of our app. Instead of dealing with the user input and visual output, it deals with APIs, Firebase libraries, databases and device sensors.
- The infrastructure layer is composed of two parts - low-level data sources and high level repositories. Additionally, this layer holds data transfer objects (DTOs).
- DTOs are classes whose sole purpose is to convert data between entities and value objects from the domain layer and the plain data of the outside world.
- DTOs can also be serialized and deserialized.
- Data sources operate at the lowest level.
- Remote data sources fit JSON response strings gotten from a server into DTOs, and also perform server requests with DTOs converted to JSON.
- Local data sources fetch data from a local database or from the device sensors.
- Repositories perform an important task of being the boundary between the domain and application layers and the ugly outside world. It's their job to take DTOs and unruly Exceptions from data sources as their input, and return nice Either<Failure, Entity> as their output.
- It's the job of the repository to perform the caching logic and orchestrate putting data from the remote data source to the local one.
- Exceptions are put into the regular flow of data as Failures. The only place for try and catch statements are Repositories. This will make it impossible not to handle exceptions, which is a very good thing.

<aside>
๐ Arrows represent the flow of data. This can be either uni-directional or bi-directional.
</aside>
<aside>
๐ The domain layer isย completely independentย of all the other layers. Just pure business logic & data. It has to have ZERO dependencies on other layers.
</aside>