By Cathryn Griffiths
I’ve been reading a lot of online commentary recently about Firebase. Mostly — it must be pointed out — written by developers who hate Firebase.
“Complex queries are impossible!” says one.
“Dumb data modeling!” says another.
“Everything has to be client-side!” complains another.
You can almost hear the little veins standing out on their foreheads.
I get it though. The issues they’ve run into are frustrating. But a big part of the problem is a lack of understanding about Firebase, what it can (and can’t!) do.
I’ve been working with Firebase over the last few months. At FFunction, we are using Firebase to build a project planning tool called Min. There are some serious misconceptions/myths/misunderstandings about this backend-as-a-service(BaaS) solution. So, I wanted to unpack some of them here.
By the way: We’re not in any way affiliated with Firebase. I want to add a little nuance to what is becoming a fairly one-sided discussion.
Misconception #1: Firebase is client-side only
Until recently, Firebase was an exclusively client-side technology. Although, it provided storage and querying capabilities. But the majority of your application logic had to exist and execute on the client. For many developers, this was a total deal breaker. After all, how many web applications these days need no backend?
Firebase team actually listened to the requests of the developer community. In March 2017, they introduced Cloud Functions for Firebase. With cloud functions, you can save snippets of code on Google Cloud. This code will run in response to specific Firebase events and HTTP requests. For example, if you want to perform data processing when saving data to the database, you can do that. Also, if you don’t want your application’s API keys exposed in your client-side code, you can do that too.
If you’re interested in learning more, I would recommend checking out the Cloud Function for Firebase docs (which have some great examples) and these recently-released tutorials.
Misconception #2: Firebase results in spaghetti code
This makes me think of that old adage: “A poor workman blames their tools!” But let’s dig into this in a little detail.
Based on my experience thus far, Firebase doesn’t result in spaghetti code. Since Firebase is mostly client side, most of your backend logic will end up on the client. If you’re not careful, you may end up with a pile of messy, unmaintainable code.
In the early stages of developing Min, we spent a lot of time planning the app. We planned how to model our data, our database structure and the best way to interact with our data. We created a connector for communicating with Firebase. It had all the code for doing CRUD operations and for interacting with Firebase. We built a collection of classes to process the object’s data according to the Firebase database structure.
This abstraction helped us keep Firebase related logic and application logic separate. Our code was maintainable and easy to debug.
Misconception #3: Dumb data modelling / too much duplication
As the Firebase team describes it, the Firebase database is just one giant JSON tree. Data gets stored as collections of key-value pairs and can have any breadth and depth that you want. There’s a lot of flexibility how you can store your data, which can get you into a lot of trouble if you’re not careful. Let me show this with an example.
Let say you’re building a basic project management application. You have users and tasks. Users can be assigned to tasks. You might want to store all task information in one database location under the tasks node:
tasks : { "001" : { name : "Development Round 1" description : "Lorem ipsum dolor sit amet elit..." startDate : "20170101" endDate : "20170201" loggedHours : { "20170101" : "1.66" "20170102" : "7" "20170103" : "5.5" } assignedStaff : "Cathryn" } "002" : { name : "Development Round 2" description : "Mauris quis turpis ut ante..." startDate : "20170206" endDate : "20170228" loggedHours : { "20170206" : "3" "20170207" : "1" "20170208" : "4.75" } assignedStaff : "Sam" } "003" : { name : "Browser Testing" description : "Vivamus nec ligula et nulla blandit..." startDate : "20170301" endDate : "20170303" loggedHours : { "20170301" : "1" "20170301" : "3" } assignedStaff : "Cathryn" }}
Now let’s say, you want to display task name of all the tasks assigned to Cathryn. To do this, you could query the database to return all tasks whose “assignedStaff” value is “Cathryn”.
firebase.database().ref(“tasks/”).orderByValue(“assignedStaff”).equalTo(“Cathryn”).once(“value”);
The problem with this query is that it’ll return all the task information for a given task that’s assigned to Cathryn, not just the task name. That’s a lot of unnecessary data to download.
To fix this, Firebase recommends you to denormalize your data(there’s a great how-to about this here). Denormalization is a process of storing redundant copies of data throughout the database, to improve read performance. In our example, we could denormalize our data by adding the following to our database:
tasksByUser : { "Cathryn" : { "001" : "Development Round 1" "003" : "Browser Testing" } "Sam" : { "002" : "Development Round 2" }}
Now if we want to retrieve the task names for all tasks assigned to Cathryn, we simply need to read from one database location:
firebase.database().ref(“/tasksByUser/Cathryn”).once(“value”);
Compared to our previous query, this will return the name of tasks assigned to Cathryn. This results in faster read operations with better performance in the long term.
Denormalization may seem like a hack to some. But it’s an imperative strategy when designing a Firebase database for a complex and scalable web application. It requires that you have a deep understanding of the data you want to store and how you’re going to use it.
Before jumping into building your Firebase database. Take the time to learn about denormalization, how to structure your data, and how to maintain consistency of denormalized data. As the Firebase team has said, “Consider that disk space is cheap, but a user’s time is not.”
If your read times are slow, then chances are that your users won’t be sticking around.
Furthermore, inefficient queries that return unnecessary data can be monetarily expensive. Depending on your Firebase pricing plan, you may need to pay as much as $1 per GB downloaded.
Misconception #4: Firebase can lead to data inconsistencies
If you’re designing your Firebase database in a correct way. Chances are your data is denormalized across multiple database locations. And if your data is stored in multiple locations, then you’re probably wondering… “how am I going to keep all that data consistent?”
In a normal case, when sending data to Firebase, you specify one database path and the data you want to store there. Let’s return to the example, to update a task name (before using denormalization), I would do this:
firebase.database().ref(“tasks/001/name”).set(“Here’s the new name”);
Now with denormalization, I may update a task’s name by doing two write operations:
firebase.database().ref(“tasks/001/name”).set(“Here’s the new name”);
firebase.database().ref(“tasksByUser/Cathryn/001”).set(“Here’s the new name”);
But doing those two write operations can lead to data inconsistencies. What if one of the write operations fails, and the other one doesn’t? What I need is an atomic write operation, allowing me to write to database paths at the same time. For this, Firebase provides multipath updates to address this exact problem. You can watch a great how-to on this here. Now, to update a task name, we just need to do the following:
firebase.database().ref().update({ “tasks/001/name” : “Here’s the new name”, “tasksByUser/Cathryn/001” : “Here’s the new name”});
If the update fails at any of the database paths, the whole update will fail. As long as you use multipath updates, your data should always be consistent.
Misconception #5: Very limited querying capabilities
Firebase has limited querying capabilities. You can sort data by keys or value and filter data by equality or using ranges.
For example, using the examples of tasks and users. I could make a query to retrieve tasks that start on, before, or after 20170601. I could also make a query to retrieve all task assigned to Cathryn, or who have hours logged for 20170203. But what I can’t do is filter by multiple values or keys. For example, I can’t make a query to retrieve the tasks that assigned to Cathryn and start after 20170601. A query to retrieve the tasks that have hours logged on 20170203 and 20170304 will also won’t work.
Querying or filtering on multiple keys or values doesn’t work, and developers love to complain about it. But if they did their research, they’d realize there’s actually a pretty good reason for it. Since Firebase is a real-time database and designed for speed, Firebase only supports operations that it can guarantee to be fast. If you want to do some complex queries, you’ll have to design your database accordingly. Complex queries aren’t impossible, you have to plan ahead for them.
For example, if I wanted to make a query to retrieve all tasks assigned to Cathryn that start on 20170201. I could add a “staff_startDate” property to my tasks, as follows:
tasks : { "001" : { ... startDate : "20170101" assignedStaff : "Cathryn" staff_startDate : “Cathryn_20170101” ... } ...}
Then, I would just need to query it this way:
firebase.database().ref(“/tasks/”).orderByChild(“staff_startDate”).equalTo(“Cathryn_20170101”);
I highly recommend you watch Common SQL Queries converted for the Firebase Database and Firebase Database Querying 101. Knowing how to structure and query your data will allow you to do more advanced queries. It will save you a lot of headaches down the road.
I decided to follow this article with a Firebase Best Practices checklist. If you put your email in the box below, I’ll send it to you.
In the meantime, I’d like to hear from other developers using Firebase.
Is anyone else liking Firebase?
Not so much?
Banging your head against the screen?
Talk to me, developers. Comments are open.