Find the deployed app here .
-
How could the service be developed to handle thousands of concurrent users with hundreds of transactions each?
Ans - Scalability is important consideration for our ledger service as our system will handle thousands of concurrent users with hundreds of transaction each. Some key factors we need to consider to make this system scalable -
- We need to make this system horizontally scalable as vertical scalability does have a limit. Like using container orchestration systems can be used to make the system easily scalable.
- Database needs to be designed efficiently. Like adding proper indexing, partitioning, sharding can heavily improve the scalability of the system.
- As this is going to be a financial service
ACID
needs to be maintained - Using caching mechanism where it can be used can improve the performance also can reduce the pressure on DB also makes the response time low hence improves the UX.
- When the system becomes big it become impossible to monitor by only logs. Need to
do proper monitoring and alerting with tools like
prometheus/grafana
. We can useSentry
like tools which can help to track bugs in production environments.
-
What has to be paid attention to if we assume that the service is going to run in multiple instances for high availability?
Ans -
- If this service runs in multiple instances to scale, it's so important
to maintain
ACID
properties because data needs to be consistent across all the instances. Otherwise, for example if we fail to maintainACID
if user's account has a balance of 100$ and multiple instance tries to add 10$ one instance will see the balance as 100$ and one will see as 110$ which will end up being a mess. - To grow confidence and reliability we need to design load testing, unit testing, integration testing, disaster recovery testing etc.
- If this service runs in multiple instances to scale, it's so important
to maintain
-
How does the the add endpoint have to be designed, if the caller cannot guarantee that it will call exactely once for the same money transfer?
Ans -
- If the caller can't guarantee that it will call exactly once for the same money transfer
it's important to design the endpoint to be idempotent. That will make sure
how many times the endpoint is being called for the transaction, the
transaction will be processed only once. We can generate a unique hash
for every transaction and save that to our transaction history and if the endpoint is called
multiple times for the same transaction identifier we can simply return
422
error or just ignore the transaction. - This endpoint needs to handle potential error in any step of the whole process of the transaction.
This endpoint should process the whole flow or discard all. That can be done by db
transaction
system. This will help to guarantee there is no inconsistency in data because of any error in any step of the whole process which we callACID
. - The way I implemented this endpoint is, it adds the balance in a transaction and does a
select
query withfor update
expression to lock the selected rows for update so that no other concurrent connection doesn't read dirty row.
- If the caller can't guarantee that it will call exactly once for the same money transfer
it's important to design the endpoint to be idempotent. That will make sure
how many times the endpoint is being called for the transaction, the
transaction will be processed only once. We can generate a unique hash
for every transaction and save that to our transaction history and if the endpoint is called
multiple times for the same transaction identifier we can simply return
make test
This opens a browser tab with graphical coverage report
make test.coverage
This will spin up docker containers and will run the e2e tests
make test.integration
This app will be running on port 8080.
-
Run this command to run this project in docker.
make development-serve
-
DB init script is already given. So tables and initial data will be loaded to db automatically.
-
6d7750a1-c3f2-4765-bf8f-33bc80f3f809
this user is automatically inserted with the help of db init script and can be used to test the apis.{ "id": "6d7750a1-c3f2-4765-bf8f-33bc80f3f809", "created_at": "2023-04-09 17:00:42.705392+00", "updated_at": "2023-04-09 17:00:42.705392+00", "name": "Test", "balance": 100 }
This endpoint is used to add balance to a user's account. this endpoint adds the balance in a transaction and does
a select
query with for update
expression to lock the selected rows for update so that no other concurrent
connection doesn't read dirty row.
Method : POST
/users/{uid}/add
Query Params:
N/A
{
"amount": 20,
"transaction_id": "tx_1as4ndakdab"
}
{
"success": true,
"message": "transaction successful!",
"status_code": 202,
"data": {
"current_balance": 160
}
}
- when transaction id was already processed. this is to handle if internal services can't guarantee caller will call only once.
{
"success": false,
"message": "transaction was already processed",
"status_code": 422
}
- invalid transaction id
{
"success": false,
"message": "valid transaction id required",
"status_code": 400
}
Provides the account balance of a user
Method : GET
/users/{uid}/balance
Query Params:
Optional: N/A
Required: N/A
{
"success": true,
"message": "request successful!",
"status_code": 200,
"data": {
"balance": 160
}
}
- User not found
{
"success": false,
"message": "user not found",
"status_code": 404
}
Provides a list of paginated transaction history for a user
Method : GET
/users/{uid}/history
page_size (ex - 10)
- page (ex -
Mw==
)
{
"success": true,
"message": "request successful!",
"status_code": 200,
"data": {
"total": 3,
"page_size": 20,
"next_page": "Mw==",
"histories": [
{
"created_at": "2023-04-09T17:54:38.305802Z",
"amount": 20,
"transaction_id": "tx_1as4ndakdab"
},
{
"created_at": "2023-04-09T17:06:21.624738Z",
"amount": 20,
"transaction_id": "tx_1as4ndakda"
},
{
"created_at": "2023-04-09T17:06:06.202434Z",
"amount": 20,
"transaction_id": "tx_1as4ndakd"
}
]
}
}
- Invalid
page_size
query param
{
"success": false,
"message": "page size should be a valid integer",
"status_code": 400
}
- Invalid
page
query param. should be a valid base64 encoded string.
{
"success": false,
"message": "invalid pagination cursor",
"status_code": 400
}