This simple microservice project is designed to provide APIs that other microservices will use so users in the gaming app can successfully perform credit/debit transactions with their wallets.
- Technologies :
- Java 11
- Spring boot
- Maven 3
- Spring Data JPA with Hibernate
- H2 Database
- JUnit
- Mockito
- Slf4j
- SwaggerUI
- Docker
- Install Java 11 and Maven 3.
- Clone the project :
git clone https://github.com/erdemakkuzu/wallet-service.git
- Go to project folder with command line and run the following command :
mvn clean package
- wallet-service.jar should have been created under the target folder. Run the .jar file with the following command:
java -jar target/wallet-service.jar
Or if you prefer to run the application inside a container, then please follow the steps below after creating the wallet-service.jar (3rd step above)
- Make sure that your docker engine is up.
- Execute the following command under the project folder. It will build a docker image.
docker build -t wallet-service.jar .
- Run the following command to see the docker images. You should be able to see wallet-service.jar image.
docker image ls
- And finally execute this command
docker run -p 8080:8080 wallet-service.jar
By clicking the link below, you should be accessing the swagger API documentation.
http://localhost:8080/swagger-ui.html
H2 in memory database was used in this project.
Console URL = http://localhost:8080/h2/
- username = sa
- password = password
- data source url = jdbc:h2:file:./data/walletDb
There is dummy data inside that you can play with after you get the project up and running. Data is not lost when you stop and restart the project. It persists data under the folder of /data/
You can examine the database structure and tables of the project from the diagram above.
To keep things simple, the number of tables is limited to 3.
Relationships between tables:
- One player can have many wallets.
- One wallet can have many transactions.
In this chapter you can find detailed information about the APIs. For swagger documenation of the APIs, visit the link = http://localhost:8080/swagger-ui.html
Here is some postman collection that includes sample requets. You can download and import with postman:
https://drive.google.com/file/d/10bUfTMPCYAp5g5wz4zN79rAP_Yqmdg22/view
1. POST player /api/players
- You can create a new player with this endpoint
Request body example:
{
"name" : "eakkuzu",
"first_name" : "Erdem",
"last_name" : "Akkuzu",
"age" : 28,
"gender" : "male"
}
- name must be unique and can't be null
- first name can't be null
- last name can't be null
Success response example (Http Status = 201 - Created):
{
"player_name": "eakkuzu",
"player_id": 2
}
Fail response example (Http Staus = 409 - Conflict):
{
"error_code": "player_already_exists",
"field": "name",
"value": "eakkuzu"
}
Note : This API does not create wallet. It only creates the player account.
2. GET player /api/players/{name}
- You can retrieve information of a specific player by giving the name of the player (player name) as a path variable.
Success response example (Http Status = 200 - OK - No wallet (New user)):
{
"id": 1,
"name": "eakkuzu",
"first_name": "Erdem",
"last_name": "Akkuzu",
"wallets": [],
"total_balance": []
}
- If total balance list and wallets list are empty, that means user does not have a wallet(s) yet.
Success response example (Http Status = 200 - OK - User has 2 wallets):
{
"id": 1,
"name": "eakkuzu",
"first_name": "Erdem",
"last_name": "Akkuzu",
"wallets": [
{
"id": 1,
"name": "Sweedish Krone wallet",
"balance": 123.0,
"currency": "SEK",
"create_date": "2022-07-30T19:55:12.207+00:00"
},
{
"id": 2,
"name": "Jackpot Wallet in USD",
"balance": 88.0,
"currency": "USD",
"create_date": "2022-07-30T19:55:29.546+00:00"
}
],
"total_balance": [
{
"currency": "USD",
"balance": 88.0
},
{
"currency": "SEK",
"balance": 123.0
}
]
}
- total_balance field indicates the player's total balances for all currencies. Player can have more than one wallets for each currency.
3. GET player /api/players
Returns the information of all players.
Success response example (Http Status = 200 - OK)
[
{
"id": 1,
"name": "eakkuzu",
"first_name": "Erdem",
"last_name": "Akkuzu",
"wallets": [
{
"id": 1,
"name": "Sweedish Krone wallet",
"balance": 123.0,
"currency": "SEK",
"create_date": "2022-07-30T19:55:12.207+00:00"
},
{
"id": 2,
"name": "Jackpot Wallet in USD",
"balance": 88.0,
"currency": "USD",
"create_date": "2022-07-30T19:55:29.546+00:00"
}
],
"total_balance": [
{
"currency": "USD",
"balance": 88.0
},
{
"currency": "SEK",
"balance": 123.0
}
]
},
{
"id": 2,
"name": "johnDoe",
"first_name": "John",
"last_name": "Doe",
"wallets": [
{
"id": 3,
"name": "Roulette wallet",
"balance": 88.0,
"currency": "USD",
"create_date": "2022-07-30T20:02:50.616+00:00"
},
{
"id": 4,
"name": "Jackpot wallet",
"balance": 72.0,
"currency": "USD",
"create_date": "2022-07-30T20:03:01.882+00:00"
}
],
"total_balance": [
{
"currency": "USD",
"balance": 160.0
}
]
}
]
4. POST Wallet /api/wallet/{playerName}
- You can create a wallet by giving playerName as parameter.
Request body example:
{
"name" : "Jackpot wallet",
"balance" : "72",
"currency" : "USD"
}
- It accepts only supported currencies (SEK, USD, TL, EU)
Success response example (Http Status = 201 - CREATED):
{
"id": 5,
"name": "Jackpot wallet",
"owner": "johnDoe",
"created_date": "2022-07-30T20:10:35.518+00:00",
"balance": 72.0,
"currency": "USD"
}
Fail response example (Http Staus = 404 - Not Found):
{
"error_code": "player_not_found",
"value": "william123"
}
5. POST Credit /api/wallet/{walletId}/credit
Credit transaction API. It takes a wallet id as a path variable.
Request body example:
{
"hash_id" : "3554s2333",
"amount" : 18,
"currency" : "SEK",
"note" : "Wanna play more "
}
- hash_id must be unique in order to achive the transaction.
- amount must be a positive number (can be decimal).
- Wallet's currency and the currency of the request must be same.
- currency must be one of the supported currencies (USD, EU, TR, SEK).
Success response example (Http Status = 201 - Created):
{
"walletId": 1,
"transaction_hash_id": "3554s2333",
"currency": "SEK",
"current_balance": 142.0
}
Fail response example (Http Status = 409 - Conflict):
{
"error_code": "non_unique_transaction_hash_id",
"value": "3554s2333"
}
Fail response example (Http Status = 400 - Bad Request):
{
"error_code": "transaction_and_wallet_currency_mismatch",
"value": "USD"
}
6. POST Debit /api/wallet/{walletId}/debit
Debit transaction API. It takes a wallet id as a path variable.
Request body example:
{
"hash_id" : "123345678c9313121",
"amount" : 11,
"currency" : "USD",
"note" : "Need some cash"
}
- hash_id must be unique in order to achive the transaction.
- amount must be a positive number (can be decimal).
- amount can not be more than balace of the wallet.
- Wallet's currency and the currency of the request must be same.
- currency must be one of the supported currencies (USD, EU, TR, SEK).
Success response example (Http Status = 201 - Created):
{
"walletId": 1,
"transaction_hash_id": "123345678c9313121",
"currency": "SEK",
"current_balance": 131.0
}
Fail response example (Http Staus = 400 - Bad Request):
{
"error_code": "not_enough_balance",
"details": "Wallet balance : 131.0"
}
7. GET Wallet transaction history /api/wallet/{walletId}/transaction-history
With this API you can retrieve the transactions of a specific wallet. Wallet Id must be given as path variable.
Success response example (Http Status = 200 - OK):
{
"id": 1,
"owner": "eakkuzu",
"name": "Sweedish Krone wallet",
"balance": 131.0,
"currency": "SEK",
"created_date": "2022-07-30T19:55:12.207+00:00",
"transactions": [
{
"hash_id": "3554233",
"amount": 1.0,
"currency": "SEK",
"type": "CREDIT",
"note": "transaction test note",
"date": "2022-07-30T20:16:28.067+00:00"
},
{
"hash_id": "3554s2333",
"amount": 18.0,
"currency": "SEK",
"type": "CREDIT",
"note": "Wanna play more ",
"date": "2022-07-30T20:18:34.069+00:00"
},
{
"hash_id": "123345678c9313121",
"amount": 11.0,
"currency": "SEK",
"type": "DEBIT",
"note": "Need some cash",
"date": "2022-07-30T20:22:01.548+00:00"
}
]
}
- Relational databases like PostgreSQL, MySql or Oracle Db can be used instead of H2 In memory database. It would be a healthier approach to write and read data to disk.
- Index strategy can be determined for tables. Although it takes up additional space, querying will become more efficient when the data becomes very large.
- JWT token can be used for authorization. If the Bearer token in the header is not valid, the request will be denied. Currently this validation logic is skipped.
- For transactions we can use a message broker such as Kafka. Transaction requests can be queued and processed by consumers subscribed to it with FIFO logic. This approach will make our application more scalable and fault tolerat.
- For some structures that don't change frequently (such as currencies) an inmemory caching library like Cache2k can be used. Once the application is up, we will inflate the cache and for each request, instead of going to db we will retrieve the data from cache.
- One more endpoint will be created for refresh cache operation. When we add a new value to db (for currency) we will trigger this endpoint.
- Also we can store some of the GET Responses (Get Player response) in cache for a short period of time (1 minute) . Key will be {playerId} and value will be GetPlayerResponse object. We won't need to query the database all the time for frequent requests in a short period of time with the same playerId.
- According to traffic we recieve, we may want to increase or decrease the running instances of wallet-service. For this case, we would use load balancer such as NGINIX.
- To redicret requets to correct insances, we should think about hashing strategy (consistent, round robin, rendezvous).
- To redirect users to correct instances, we should generate the hash with accurate instances. (such as player name and instance name etc.) With this strategy, we will use caches of the wallet-service instances in a efficient way even if we decrease or increase the number of instances.
- To track the status of the application, we can use logging tools such as NewRelic.
- By using the metric registries we can recieve detailed information about caches etc.
- We can think of a structre that holds status of each transaction attempt. With this feature
when we use GET /api/wallet/{walletNumber}/transaction-history API, it will retrieve extra information
such as:
- status : "SUCCESS" OR "FAILED"
- fail_reason : null OR "non_unique_transaction_id"
- Currently we don't validate hash_id format for transaction. In future it can be obtained from another microservice in UUID format. Then we will validate if it's unique or not.
- Using BigDecimal data type is safer way than performing operations with Double data type. We can replace our Double data types (amount, balance etc.) with BigDecimal in the future.
-
Currently we don't support a transaction attempt which has different currency type in a target wallet. To implement this feature, we can store currency rate information in another table and update it daily. When we recieve a cross-currency transaction attempt, we will be able to convert between currencies and perform the transaction.
-
Currency values are currently stored in an enum. A more detailed approach can be considered. Current valid currencies = EU, USD, SEK, TL
- BDD testing tools such as Cucumber can be used.
- Creating Jenkins jobs which runs Unit Tests and Cucumber Tests when a new commit merged, will inform us about potential defects in the application.
- For each requets we recieve, we can store the request and response in our storage as Json Blob.
- It will be easier to keep track of past requests.
- For example if user enters String value instead of long value, it shows internal service excepion . More detailed error code can be shown to user.
- API responses can be paginated. This approach will come in handy when dealing with very large data.
- Javadocs can be created for all the methods.
- Mapping tools can be used to map objects (from entity to dtos or reverse) such as Orika Mapper.