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 Streams
  • Extensive data validation for a rich user experience
  • Google & email + password authentication
  • Reorderable todo lists and much more

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.

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).
Flutter Firebase & DDD

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.

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

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!

#flutter #firebases #webdev

Flutter Firebase & DDD Course [1] – Domain-Driven Design Principles
27.30 GEEK