Nice Strategy to Handle Multiple Requests to One Database
Introduction
Since this is my first proper blog on the topic, I would love to introduce myself to you.
My name is Ephraim Umunnakwe. I am a Mobile Developer and Backend Engineer. My friends call me Leo, but my nickname is KingRaym. I am passionate about building high-performing apps (both server and client side) and creating beautiful UIs. I started coding in 2016 after falling in love with the C language, influenced by my elder brother. Since then, I've had quite a journey. π I spent the first three years experimenting with different programming languages before starting my professional career as a Mobile Developer (Android and Flutter) in 2019. In 2022, I decided to explore Backend Engineering as a hobby, learning Node.js from the Genesys cohort and SpringBoot. I have taught about eight students at a tech education institution, worked for a couple of startups, built some with my friends, and I'm currently building my startup Raym Universe Ltd.
I saw the opportunity provided by the HNG Internship and decided it would be amazing to join this family to increase my knowledge and reach. I've always heard about it. I think I joined and dropped out around 2019/2020, but this time, I'm coming back stronger and better. I believe nothing can stop me at this point. I've even gathered some of my friends so we can motivate each other. I know how difficult it can be, but I believe it is part of building professional discipline.
You can learn more about the internship here
Also check this link to find and hire talents. Hopefully you hire meπ
Now that you know about me, let's cut to the case shall we?
The colour of the problem π
This was a collab. project with a friend who is a Product Designer. He designed an event reservation UI, and I decided to work on the backend to practice and improve my skills.
Everything was going well until I started testing. It turns out that when multiple people book events and the seats run out, the data isn't consistent. During testing, I discovered that someone could overbook the seats due to inefficient handling. If two people try to book an event with 200 seats and each book 150 seats at the same time, it would go through, which shouldn't happen. There had to be a way to handle that case.
So what did I do?
So, I had to make changes to my database structure and how I access the seat booking data.
Technologies used
Node.js
Firebase Firestore
I used Firestore because it's a personal favourite you can also use SQL for this, I set it up in order to handle the concurrent requests that come in and ensure data consistency across the different clients at the same time.
Step-by-Step Solution:
I made use of a technique called concurrency control!
Note: If you don't understand optimistic concurrency control or Firestore setup, check the references section at the end of this article.
Firestore Setup:
Learn about Firestore setup here. I won't go into detail here to keep this article concise. (It's quite easy π) Next, we initialize our admin task and our
db
variable, which represents our Firestore database instance.Firestore Initialization:
The Firebase Admin SDK is initialized with your service account credentials. To learn how to get your service account key click here.
const serviceAccount = require('./path/to/serviceAccountKey.json'); //initialize admin admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }); //then firestore data base is initialized and set to our DB variable const db = admin.firestore();
Optimistic Concurrency Control:
This is handled using Firebase transactions with two sub-steps as shown below;
Transaction Management:
Within our try block, we begin the Firebase transaction using the
db.runTransaction
functionawait db.runTransaction(async (transaction) => { //as the transaction is running we get the particular // transaction using it's reference const eventDoc = await transaction.get(eventRef); if (!eventDoc.exists) { throw new Error('The even you requested is not found'); } //if the document doesn't exit it will throw the error above, however, //if it does exist we would now compare the number of available seats //with the avaialble tickets const eventData = eventDoc.data(); if (eventData.availableSeats < tickets) { throw new Error('Not enough seats available'); } //if the if the seats are stil available it would update the available //seats by removing the current number of tickets from the already //published number of seats const updatedSeats = eventData.availableSeats - tickets; //transaction.update() will then update the seats within our response. transaction.update(eventRef, { availableSeats: updatedSeats }); }); //return response to the user, we would use 200 since it's OK res.status(200).json({ message: 'Booking successful' });
What about the Optimistic Concurrency Control?
Well, Firestore transactions automatically handle concurrency by retrying the transaction if there is any conflict, so our problem is solved. It's like setting a watcher for each user's record to ensure their order doesn't exceed what is available. By using transactions, we ensure that even with high concurrency (simultaneous users), the available seats count remains consistent and correct.
This is the JSON Model structure to give you a better understanding of what I worked with.
{
"events": {
"ox3344dddfifhsnohosshn01s": {
"name": "Taylor Swift even",
"totalSeats": 100,
"availableSeats": 50
}
}
}
- Optimizing Booking Endpoint:
I had to optimize my booking endpoint because it wasn't updating and keeping track of available seats as it should. At least, that's what I thought π . This solution worked better.
const express = require('express');
const admin = require('firebase-admin');
const app = express();
const port = 3000;
const serviceAccount = require('./path/to/serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
const db = admin.firestore();
app.use(express.json());
app.post('/book-ticket', async (req, res) => {
const { eventId, tickets } = req.body;
const eventRef = db.collection('events').doc(eventId);
try {
await db.runTransaction(async (transaction) => {
const eventDoc = await transaction.get(eventRef);
if (!eventDoc.exists) {
throw new Error('Event not found');
}
const eventData = eventDoc.data();
if (eventData.availableSeats < tickets) {
throw new Error('Not enough seats available');
}
const updatedSeats = eventData.availableSeats - tickets;
transaction.update(eventRef, { availableSeats: updatedSeats });
});
res.status(200).json({ message: 'Booking successful' });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
Attention
Please don't forget to use try-catch
to catch errors and your Firestore security rules to prevent intruders from manipulating the normal flow of the API.
References for your closer study
Optimistic Concurrency on Wiki
Optimistic Concurrency explanation from stack overflow
Remember to look into my blog and follow on SM too for more articles like this!
THANK YOU! β€
Subscribe to my newsletter
Read articles from Umunnakwe Ephraim Ekene directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by