One of my clients was requesting to have a comments feature built into an existing mobile app where users would be able to express their opinions on posts. I had to satisfy the following requirements considering that it was an early stage startup:
- Cost effective
- Long Term Scalability
I knew I had to start with a graph database to be able to support this feature and consider future growth. Firestore was just not going to align with future scalability given that we would eventually need aggregation features as well as search and recommendations support. Having surveyed several options on the market, ArangoDB positioned itself as a strong solution given the maturity of the product and being open source.
The end result was a single instance deployment of arangoDB on Google Compute Engine costing about $40 a month.
This is a high level overview of the graph structure:
In this example we are using the link package to interact with arangoDB. We use a transaction to ensure necessary nodes and edges are created.
try { const arangoClient = await arangoDBClient; const commentsCollection = arangoClient.collection('comments'); const postsCollection = arangoClient.collection('posts'); const usersCollection = arangoClient.collection('users'); const relationsEdge = arangoClient.collection('relations'); const { verified_uid } = req; const { postId, comment } = req.body; const trx = await arangoClient.beginTransaction({ write: [commentsCollection, postsCollection, usersCollection, relationsEdge], }); const commentNode = await trx.step(() => arangoClient.query(aql` INSERT { comment: ${comment}, userId: ${verified_uid}, display: ${true}, createdAt: DATE_NOW() } INTO ${commentsCollection} LET inserted = NEW RETURN inserted._key `) ); const commentNodeResult = commentNode ? await commentNode.all() : []; const commentNodeId = commentNodeResult.length ? commentNodeResult[0] : null; // upsert the post node since the post might already exist await trx.step(() => arangoClient.query(aql` UPSERT { _key: ${postId} } INSERT { _key: ${postId}, following: 0, comments: 1, createdAt: DATE_NOW() } UPDATE { comments: OLD.comments + 1, updatedAt: DATE_NOW() } IN ${postsCollection} RETURN { doc: NEW, type: OLD ? 'update' : 'insert' } `) ); // upsert the user node since the user might already exist await trx.step(() => arangoClient.query(aql` UPSERT { _key: ${verified_uid} } INSERT { _key: ${verified_uid}, following: 0, comments: 1, createdAt: DATE_NOW() } UPDATE { comments: OLD.comments + 1, updatedAt: DATE_NOW() } IN ${usersCollection} RETURN { doc: NEW, type: OLD ? 'update' : 'insert' } `) ); const fromPost = `${POSTS_DOC_COLLECTION}/${postId}`; const fromUser = `${USERS_DOC_COLLECTION}/${verified_uid}`; const toComment = `${COMMENTS_DOC_COLLECTION}/${commentNodeId}`; // create HAS_COMMENT edge from post node to comment node await trx.step(() => arangoClient.query(aql` INSERT { _from: ${fromPost}, _to: ${toComment}, edge: ${'HAS_COMMENT'}, createdAt: DATE_NOW() } INTO ${relationsEdge} `) ); // create CREATES_COMMENT edge from user node to comment node await trx.step(() => arangoClient.query(aql` INSERT { _from: ${fromUser}, _to: ${toComment}, edge: ${'CREATES_COMMENT'}, createdAt: DATE_NOW() } INTO ${relationsEdge} `) ); await trx.commit(); return res.status(201).json({ id: commentNodeId }); } catch (error) { console.error(error); return res.status(500).json({ type: error.name, message: error.message }); }
As this feature matures there is potential for growth in several areas:
- Setup of an Active Failover instance to support a more robust deployment
- Auto censor comments
- Support comment reply