Firebase ? allows developers to get their apps working quickly. Flutter ? does the same thing. But, as it goes in life, trade-offs are everywhere. Sure, you can hack something together in a dash of enthusiasm but this initial excitement will fade away as soon as you get totally lost in your own code.
Flutter apps need structure that is easy to orient yourself in, testable and maintainable. It also wouldn't hurt if the way you architect your Flutter apps allows for adding new features without a headache. Especially with a client-centric service such as Firebase Firestore, it's extremely important to keep your code clean. Let's do it by following the principles of Domain-Driven Design.
The project we will build
We're going to build a fairly complex note taking application featuring things such as
- Real-time Firestore data
Stream
s - Extensive data validation for a rich user experience
- Google & email + password authentication
- Reorderable todo lists and much more
A video is worth a thousand words, as they say, so before getting into the details of how DDD fits into all of this...
Domain-Driven Design - the big picture
You have surely seen or built Flutter apps using Firebase Auth and Firestore and I'm sure you didn't put the Firebase calls into your widget code. In other words, you used some sort of an architecture.
You may have even watched my Clean Architecture course where we worked with a simple REST API. Over there, the class dependency flow together with the file and folder structure were neatly outlined for you and it's possible to use it with Firebase too. How is Domain-Driven Design (DDD) different then?
Simply said, it's better on every level. We still have a good separation into layers which brings beautiful traits ease of navigation and testability. So no, we still don't put all of our code into the UI or state management.
We will go over everything in more detail later but for now just know that the diagram below outlines the key architectural layers present in a DDD Flutter app. We're going to use BLoC in this series but, as usual, I didn't forget about all of you who have declared BLoC a sworn enemy. If you don't want to use it in your apps, feel free to use a view model of your choice, whether it be ChangeNotifier
, MobX Store
, or even the new StateNotifier
. More on that later.
There are a few things that I couldn't fit on the diagram. Namely:
- Arrows represent the flow of data. This can be either uni-directional or bi-directional.
- The domain layer is completely independent of all the other layers. Just pure business logic & data.
If you're familiar with my Clean Architecture course, the above diagram feels somewhat familiar. If you aren't, no worries. It's not a prerequisite for this tutorial series. Note that classes such as Firestore
or FirebaseAuth
are ready-made data sources, so we will write code from repositories upwards.
Entities
and validated ValueObjects
also contain logic. This ranges from data validation and helpers to complex computations.Exception
s are put into the regular flow of data as Failure
s. 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.Before we go ahead and take a closer look at what are the roles of all the layers and their classes, let's first tackle the age old question of folder structure.
Folder structure
I'm the first to admit that the folder structure outlined in the Clean Architecture course is a pain to deal with. With DDD, we're going to take a different approach.
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!
Let's take a look at the notes feature. While the main notes folder is present inside every layer (application, domain, infrastructure, presentation), its subfolders are different!
What does this mean? Well, we can have both a good folder structure and a separation into architectural layers at the same time ??
It's also worth noting that some features don't even have to necessarily be represented in all layers. Notice that splash folder in the presentation layer? There is no inherent "splash screen logic", so it doesn't make sense to put it into other layers.
All in all, we can mix and match the dependencies in between features as long as we keep true to the dependency flow outlined in the diagram above (domain layer has to have ZERO dependencies on other layers).
Architectural Layers
Unlike with the spaghetti architecture ?, you will always know where to put a certain class when you're following the Domain-Driven Design principles. Each one of the layers has a clear-cut responsibility. As you can see on the folder structure picture above, every architectural layer contains features and possibly a core folder which holds classes common to all the features in that layer (helpers, abstract classes, ...).
Presentation
This layer is all Widgets ? and also the state of the Widget
s. I already mentioned that we're going to use BLoC in this series. If you're not familiar with this state management pattern, I'd recommend you to check out this tutorial. The main difference from something like a ChangeNotifier
is that BLoCs are separated into 3 core components:
- States - Their sole purpose is to deliver values (variables) to the widgets.
- Events - Equivalent to methods inside a
ChangeNotifier
. These trigger logic inside the BLoC and can optionally carry some raw data (e.g.String
from aTextField
) to the BLoC. - BLoC - NOT A PART OF THE PRESENTATION LAYER!!! But it executes logic based on the incoming events and then it outputs states.
So, if you're not using BLoC, just replace the State
and Event
classes with a single View Model class of your choice.
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. Its logic is limited to creating "eye candy" for the user. So while animation code does belong into this layer, even things like form validation are NOT done inside the presentation layer.
Application
This layer is away from all of the outside interfaces of an app. You aren't going to find any UI code, network code, or database code here. 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.
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 Stream
s (not directly, but by utilizing the dependency inversion principle, more on that later).
UseCase
classes. Learn more from the mysterious uncle.Domain
The domain layer is the pristine center of an app. It is fully self contained and it doesn't depend on any other layers. Domain 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. Because domain doesn't depend on anything external, changes to such implementation details don't affect it. On the other hand, all the other layers do depend on domain.
So, what exactly goes on inside the domain layer? This is where your business logic lives. We are going to get to everything in detail in the next parts of this series, but everything which is not Flutter/server/device dependent goes into domain. This includes:
- Validating data and keeping it valid with
ValueObject
s. For example, instead of using a plainString
for the body of aNote
, we're going to have a separate class calledNoteBody
. It will encapsulate aString
value and make sure that it's no more than 1000 characters long and that it's not empty.
EmailAddress
and Password
encapsulate a String
, it will be impossible to pass an EmailAddress
to a method expecting a Password
argument and vice versa.- Transforming data (e.g. make any color fully opaque).
- Grouping and uniquely identifying data that belongs together through
Entity
classes (e.g.User
orNote
entities) - Performing complex business logic - this is not necessarily always the case in client Flutter apps, since you should leave complex logic to the server. Although, if you're building a truly serverless ? app, this is where you'd put that logic.
In addition to all this, the domain layer is also the home of Failure
s. Handling exceptions is a ? experience. They're flying left and right across multiple layers of code. You have to check documentation (even your own one) a million times to know which method throws which exception. Even then, if you come back to your code in a few months, you're going to be again unsure if you handled all exceptional cases.
We want to mitigate this pain with union types! Instead of using the return
keyword for "correct" data and the throw
keyword when something goes wrong, we're going to have Failure
unions. This will also ensure that we'll know about a method's possible failures without checking the documentation. Again, we're going to get to the details in the next parts.
Infrastructure
Much like presentation, this layer is also at the boundary of our app. Although, of course, it's at the "opposite end" and 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). Let's break it down!
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. As you know, only dumb data like String
or int
can be stored inside Firestore but we don't want this kind of unvalidated data throughout our app. That's precisely why we'll use ValueObject
s described above everywhere, except for the infractructure layer. 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. Similarly, 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 Exception
s from data sources as their input, and return nice Either<Failure, Entity>
as their output.
Either
, you'll find out more about it in the next parts. Another option is to read this Kotlin documentation if you are impatient ?If you don't use Firebase Firestore, you'll probably need to cache data locally yourself. In that case, 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.
Code is coming
If you made it through this information-packed article, I salute you. Although we'd all like to immediately start coding, this tendency is precisely what causes spaghetti code and spaghetti architecture to rise ?. Having at least this preliminary information in mind when we start coding in the next part (after setting up Firebase), I'll then guide you through the intricate details as we go. One may say that AOT learning will be complemented by JIT explanations. Yes, I know I should get some fresh air... See you in the next part!
Greetings, thanks for your super tutorials. Whenever you start a series of tutorials I wonder if you have already written the entire series. and if so and if I invite you a coffee, could I get the whole series?
I did write the whole app but this is the only article I’ve written. I’m going to write the next one for this Friday.
In that case, could you possibly just release the whole code base right now? I learn a lot just by looking at how someone structures something and the way they’ve coded it. Plus it would help out us impatient guys…
I will release the whole project repository on Friday. Then there will be a separate repository that will contain code on a per-episode basis. You’ll have to keep in mind though that the “finished” project is not yet fully ironed out in certain areas.
0k, thanks for answering, could you share the full code ?, then you could sell your courses that way. It’s just an idea. Again thanks for your great tutorials
Hey Metaj, I was about to email you about getting the full code but saw this chain of comments. You always release the code for the other tutorials, so I assume there is a reason behind not having it available right now. If you could make it public on the ResoCoder github account that would be quite lovely. 🙂 thanks again, I’m really grateful you are putting out such good tutorials!
Hello Tanner! I’ll release it on Friday as soon as we actually write some code ? Also, I’m going to release the finished project repo so that you can have a look at the final code before we finish this series. I don’t want to leave you all hanging for 2 months (it’s gonna be a long series ?).
you the man!
I’m impatiently waiting for the next one. ?
Nice series! I’m looking forward for this! <3
Thank you very much, i really appreciate your work. I deeply studied your tutorial on clean architecture and i’m developing a project with that paradigm. My project involves FirebaseAuth and Firestore so i got a bit stucked. I think i’ll move the project to DDD. Can’t wait to check out the code.
Hey Vittorio,
Although the course will take a long time to write and record, you can check out the finished project even now (without explanation). I’m sure it’ll help a bit. https://github.com/ResoCoder/finished-flutter-firebase-ddd-course
Hey matt!! thank you very much 🙂 i studied your code and i’m adapting my project according to it. I’m trying to mix a bit the infrastructure concept with the one of datasource of clean architecture. I’ld like to show you something about the usecase i worked on, is it possible to contact you on skype?
It will be better if you write to me here or maybe contact me via email.
Great stuff! I have been following your tutorials and especially your clean architecture. Agree that having top level directories per ferature can be problematic especially for presentation level where some UIs need to present multiple features on same page. Looking forward to your future tutorials!
By the way, if i would like to optimize my firestore read for the sake of cost, will this architecture handle it in domain/infrastructure level or will I still have to modify my presentation level?
I’m glad you like it ? Firestore code will reside only inside the infrastructure layer.
Hi Matt,
Thanks for a a great tutorial once again!
I have a question about entities: is it possible for them to reference other entities. For example can a Book entity have an author property that references an Author entity? I think I’ve read somewhere that is is a bad idea but I can’t find it anymore and if so what is the reason.
Hey Gilles!
I’m not sure why that should be a bad idea. How else are you going to model relationships?
Of course, all of the relational DB foreign keys and stuff would be handled in the infrastructure layer, not in the domain entities.
on what way is DDD better than Clean Architecture?
The folder structure is much better for sure. We’re also going to have a better way of converting models (data transfer objects) to entities.
tbh i dont agree. i prefer folder structure from clean architecture where you keep your bloc in the same folder as your page is. it’s way easier to find.
Hey! The quality of your flutter material is so damn good it makes water stuff come out of my eyes.
Anyway, I have similar questions as Younes Henni. I think many people will be coming from your Clean Code course. So if you could point out the similarities and differences between the 2 architecture approaches, I would excrete more water stuff! (Water stuff of joy)
I meant; explain the similarities and differences going forward from now in the videos. Thanks again bro!
I’ll do my best.
Please do! For example, you can record/write an introductory video/article to explain the thought process behind DDD (based on the work of Eric Evans et al.). It is certainly not just a better folder structure or DTO conversion.
Well, I kind of did just that by introducing the validated ValueObjects.
Thanks man. I look forward to every video.
Hi, is there any reason why you use BLoC instead of States Rebuilder? I just came from your tutorial on that one.
I started to dislike states_rebuilder because it’s just too “magical” in my humble opinion. The new state_notifier package looks good though. Regardless of that, we’re going to do this series with BLoC.
Hi, Matt, thanks for replying. You meant ‘magical’ because we can’t see how it works inside the code?
Hey Matt!
My concern is about Application layer for those who use BLoC and those who use for instance ChangeNotifier + Provider with MVVM pattern.
You’re saying:
“If you’re not using BLoC, do yourself a favor, and don’t put the application logic into View Models. I’d recommend creating reusable, one-purpose UseCase classes. Learn more from the mysterious uncle.”
Looks like discrimination 🙂
As far as I know, it’s recommended to have one BLoC class for each page. You get the streams of changes and Events from UI there and decide what’s the next state should be and yield it.
UI listens to the state changes and rebuilds itself correspondingly. All good.
So what’s the difference if instead BLoC class we have ChangNotifier class? It gets business logic streams and inputs from UI (by calling its methods), decides what’s the next state should be and yield the states by calling notifyListeners() or emit Stream events.
UI listens to the state changes (enum for instance) and rebuilds itself correspondingly. All good. Why do bother creating dozens of separate UseCase classes?
Thanks
Hello Kirill,
The “one BLoC per page” rule is not set in stone and we’re going to have as many as three BLoCs per page, each focused on a different part of dealing with notes.
I find that BLoCs are nicely composable precisely because they use Events for dispatching actions and they also separate out their data into separate classes (States). In other words, having a BLoC depending on another BLoC is much easier to comprehend than having two ChangeNotifiers be dependent on one another (IMO).
That’s what I do in client projects too. I’m not using BLoC everywhere and having a use case which depends on 3 different repositories is easy to understand. Having a cluttered view model is much harder to find your way through.
In the end, feel free to experiment and let me know about your findings ?
Hi! do you have a code snippet about how implement a normal API in the infrastructure layer? I mean, with firebase is more easier because is a clientAPI but, if I have my own API? Thanks! a lot
Hey Matt,
If you were to start a large project, which architecture would you use (DDD or CA)?
Thanks for your great tutorials, I love them and I read them all.
I have a concern about project folder structure:
Uncle bob promoting a concept named Screaming Architecture. which says your project folder structure should say what the app is about instead of opening each folder and looking at the classes to find out.
Quoted from Uncle Bob: ” When you look at the top level directory structure, and the source files in the highest level package; do they scream: Health Care System, or Accounting System, or Inventory Management System? Or do they scream: Rails, or Spring/Hibernate, or ASP?”
I have to compile and run it in my mind to figure out what it does. What’s worse, I have to expand all the folders to start investigating.
So how do you think the project structure should improved?
Yeah, I agree strongly with that one.
I have one question regarding entities vs DTOs. It may happen that there’s this class in the domain which is fine on its own but when we get to reference it saving it or reading later from an external API, how could we handle the ID as it’s not present in the entity but required in the DTO.
Salute you for your good effort for such a good article…
Hi Matt,
Great tutorials. I am facing one issue with this architecture. The flutter inspector isn’t working somehow and when I enable “widget select mode” on any screen in the app it shows me the circular loader, which is what we show in the splash screen.
I think the DDD architecture is better suited for flutter apps than the TDD since the folder structure is more straightforward and intuitive. But there is no testing at all which is not acceptable for production. Any plans to add testing to this project or do you have any ideas how to add testing. Would love to use DDD over TDD but only with test coverage. Could provide some ideas to add tests to this project – it would be great to have a last part were you add some tests to the project.
Thank you. Keep up the great work!
Hi Matt,
All my respect, you’re one of the very rare species in the Developers community who is seeking for deep understanding and try to find the best and simple solutions and ofc thanks all your tutorials.
Anyway, I have some questions:
1. Do you prefer TDD + Clean to DDD in flutter?
2. What is your thoughts on some hybrid style development like TDD+BDD+DDD in flutter?
3. How would you split a complex app to different dart or flutter packages by the used services/features/storyboards (like in Flutter Architecture Samples, though it has only a very simple splitting to only repository or like services in https://github.com/dotnet-architecture/eShopOnContainers).
4. Also, as flutter was initially built for mobile apps in reactive programming in mind, that needs some different type architectural patterns than the old MVC/MVP/MVVM etc., so like RxVM /w RxCommand (for example achieving to retrieve only read-only data from some underlying cache/db i.e. as some CQRS like solution) or similar. Which one do you recommend or prefer (from like the Flutter Architecture Samples’ examples)?
Thx in advance.
Cheers,
Pal
sir this is very cool, can you make another using Provider???
Thanks matt
Hi Matt,
Thank you very much for the amazing tutorials.
About entities : would you use an entity as a value object of another entity ?
For example if one property of note is “owner” and you want to fill it with a user, would you use directly the user entity or would you create a new value object ?
Thank you very much
Would you still recommend this tutorial in 2021?
Hi Matt,
can you explain in more detail why this new folder structure ist better than the one in the “Clean Code” tutorial. I thinks it is much easier to navigate throught the code if the project contains the feature-folders first and then the layers underneath.
Thank you for the great tutorials!
Hello, very interesting and thank you for the bulk of informations! Currently I am working with this approach: MVC+S https://blog.gskinner.com/archives/2020/09/flutter-state-management-with-mvcs.html
It would be realy interesting if u can compare these 2! ..just a idea 😉 Thank!
Hi, I’m following your flutter-firebase-DDD course these days. But I can not complete it because I can not go to the written tutorials in the course. The page loads only for the 9th tutorial of your blog and I can not find the rest of the written tutorials in your blog. If you would like to share the link to those 32 written tutorials I would appreciate it.
why not to watch it on YouTube?
Faster to read
Thank you for the awesome tutorials. I have a question. Why is your BLoc considered a use case on DDD but not on Clean Architecture. In clean architecture your bloc uses a Use Case.
Clean Architecture or this one? for the project currently on firebase but will soon be migrated to a custom backend. TIA
Nice architecture, Matt. Looking forward to the code.
Thank you very much Matt, this is such a great source of information for Flutter development!
I don’t really agree on what you said about serverless when talking about the Domain Layer and handling complex business logic: “Although, if you’re building a truly serverless ? app, this is where you’d put that logic.”
I think that frontend development should not include actual business logic, otherwise you are building some kind of monolith. In serverless, business logic is typically on lambdas, who can scalate and be managed independently from the frontend technologies.
Best regards!
Hi Guillermo,
If you don’t use lambdas or other cloud functions in general, and you just rely on Firebase Firestore or AppWrite DataStore, then what he said is right!
Not all serverless mean lamdas (in fact that is just an overhyped marketing slogan since lamdas actually run on servers outside of your maintenance, and you still have to technically write the business logic, whereas apps that rely only on firestores/datastores have the business logic in-app. Datastores only act as remote infrastructures rather than local, firestores act as both remote and local, and they are both used for data persistence only. In actual sense, this is what serverless means because none of your code/business logic is being run in a remote machine on the cloud or should I say, “in a remote server you do not maintain’?).
All of these approaches have their use cases, for example, if you don’t want your code to be reversed engineered make sure you go with lamdas serverful ha ha. If reverse engineering does not bother you, go with in-app serverless approach.
When you make a tutorial, I feel that the tutorial should cover just that one topic. Your tutorial has a steep understanding curve because you have mixed in several topics into one tutorial, making it useless for me at least.
Can you be more specific about the content of your article? After reading it, I still have some doubts. Hope you can help me.