Design a RESTFul API system for a vending machine, allowing users with a “seller” role to add, update or remove products, while users with a “buyer” role can deposit coins into the machine and make purchases.
The vending machine should only accept 5, 10, 20, 50 and 100 cent coins.
npm run dev # run the API in development mode
npm run test # test using Jest
npm run coverage # test and open the coverage report in the browser
npm run lint # lint using ESLint
npm run docs # generate API docs
First, you will need to install and run MongoDB in another terminal instance. You need to run the MongoDB server as a ReplicaSet in order to be able to use the Transactions feature.
Please, check this tutorial for more information on how to convert a standalone MongoDB server to a single-node replica set.
mongod --replSet rs0 --dbpath /data/db --port 27017
$ rs.initiate()
Then, run the server in development mode.
npm install
npm run dev
Express server listening on http://0.0.0.0:9000, in development mode
To run the application locally, you can use the Docker Compose tool.
docker-compose up -f docker-compose.local.yaml
Express server listening on http://0.0.0.0:9000, in compose mode
And to run the tests locally using Docker Compose.
docker-compose up -f docker-compose.test.yaml
You can find the API documentation in the DOCS.md file. This file is autogenerated by the apiDoc inline APIs documentation tool.
How we can guarantee data consistency in the /buy
endpoint?
-
Pessimistic Locking: In MongoDB, a write operation is atomic on the level of a single document/record, but not on the level of a collection. Read more about Atomicity and Transactions.
-
Optimistic Locking: Ensuring that the document/record wasn't updated by some other write operation by checking the locking key before updating/writing the document back using the
findAndUpdateOne()
method. Read more about Atomic Updates using findOneAndUpdate. -
Transactions: MongoDB supports multi-document transactions (in a single or multiple collections). So decreasing the product's stock, decreasing the user's balance, and creating the order document/record itself are all part of the same transaction. Read more about Transactions.
const product = await Product.findOneAndUpdate({ _id: productId, amount: { $gte: amount } }, ... }, { session })
...
const buyer = await User.findOneAndUpdate({ _id: user.id, deposit: { $gte: cost } }, ... }, { session })
...
const order = new Order({ buyerId: buyer, productId: product, amount, cost })
await order.save({ session })
...