36  comments

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.

This post is a part of a series. See all the parts 👉 here 👈

The project we will build

We're going to build a fairly complex note taking application featuring things such as

  • Real-time Firestore data Streams
  • 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.

Notice that in addition to holding and carrying around data, Entities and validated ValueObjects also contain logic. This ranges from data validation and helpers to complex computations.
Also take note of how 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.

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 Widgets. 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 a TextField) 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.

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.

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 arrow are Events which are sent from the presentation 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 Streams (not directly, but by utilizing the dependency inversion principle, more on that later).

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.

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 ValueObjects. For example, instead of using a plain String for the body of a Note, we're going to have a separate class called NoteBody. It will encapsulate a String value and make sure that it's no more than 1000 characters long and that it's not empty.
This kind of validation is usually reserved for the UI. We're, however, going to take advantage of the Dart type system to its full extent. For example, although both 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 or Note 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.
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. This makes sense - you're probably not changing the business logic on a daily basis.

In addition to all this, the domain layer is also the home of Failures. 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 ValueObjects 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.

Firebase client libraries like cloud_firestore and firebase_auth  do the heavy lifting of data sources for us. That's why we won't create any data sources in this course.

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. 

If this is the first time you hear about 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!

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a Flutter freelancer and most importantly developer educator, he doesn't have a lot of free time 😅 Yet he still manages to squeeze in tough workouts 💪

You may also like

Flutter Bloc & Cubit Tutorial

Flutter Custom & Staggered Page Transition Animation Tutorial

  • 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?

      • 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!

  • 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.

  • 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?

  • 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.

  • 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)

  • 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

  • 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?

  • 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.

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >