The Credit Transfers API application aims to perform transactions between bank accounts. Currently, the application contains only one use case related to sending payments from Qonto customers to other bank accounts.
Given the time constraints for the challenge, it was decided to use Rails as a web framework and SQLite3 as the proposed database to save time in the initial project setup and focus on developing the functionality and organization of the application.
The project is essentially organized into three layers:
- API layer - where controllers as adapters for HTTP requests are located.
- Application layer - where application use cases and some objects encapsulating input data are located
- Domain and database layer - where entities like
BankAccount
andTransfer
are located. I would like to separate these two responsibilities and have a business layer separate from the database adaptation layer, for instance. However, given the time limit for the challenge, I decided to follow a simpler approach already proposed by the framework. Thus, the application flow will always beControllers -> UseCases -> Models
. Models do not depend on UseCases, and UseCases do not depend on Controllers. All dependencies are managed through dependency injection, facilitating unit tests.
Given that the JSON files used as samples provide bank account data in the request body, it was decided to create the /transfers
resource instead of the /bank_account/:id/transfers
resource.
I decided to use the concept of Commands to encapsulate input data from HTTP calls. Some validations should have been implemented but were skipped due to time constraints.
UseCases are the orchestrators of the application. They manage database transactions, such as in the process of creating transfers and updating the financial balance, and prepare the data to be sent for business rule execution.
As mentioned earlier, the use of the Rails framework brings some coupling problems in the Models layer. Here, classes are responsible for serving as an entity for connection and mapping with database tables and also for executing business rules.
The application has two types of tests:
- Unit tests: used in the UseCase layer for easy manipulation of dependencies and testing different scenarios using mocks.
- Integration tests: controllers are tested in an integrated way, without using mocks. This brings the benefit of testing important features of the application in its real state.
- Transaction Isolation Level: The transaction opened in the process of creating transfers and updating the financial balance has the lowest isolation level,
read_uncommitted
. My intention, considering a real scenario of an application running on multiple nodes using the same database, is to use a safer and more restricted isolation level for such a critical operation.Serializable
would be my choice. However, when changing the isolation level, I noticed that SQLite3 does not support this type of isolation by default:"SQLite3 only supports the read_uncommitted transaction isolation level."
To make this change, it would be necessary to allow theshared_cache
of the database, which can bring some benefits like performance but also brings some problems such as the complexity of managing this shared cache in distributed systems and some concurrency issues. Then, I decided to move with the lowest level and explain my approach in the Improvements section. - Input values for the
amount
attribute: I had a lot of indecision about the values received in the amount field of the JSON request. My doubt was whether values without decimal places should be considered as if they had the decimal place or not, for example, if 98234 would be 98234.0 or 982.34. I decided to go with the first option.
Good improvements could be applied to the application if there were more time or team collaboration.
- Use of a relational database such as PostgreSQL, for example, mainly for using transactions with other isolation levels.
- Addition of constraints on database tables, such as avoiding the entry of null values.
- Validation of input data, such as checking if the amount field value is positive, and preventing SQL injection for security reasons.
- Addition of more logs throughout the application.
- Change the resource
/transfers
in the API to a nested resource/bank_account/:id/transfers
and avoid receiving the raw bank account data in the request body. - Better API responses for
/transfers
endpoint. - Migration to a Hexagonal architecture (domains/repositories).
- Docker and docker-compose for easier local execution.
Dependencies:
- Ruby version 3.1.1
First, install the project dependencies:
bundle install
Add the qonto_accounts.sqlite3
file to the db/
folder and change the value of the database
key in the database.yml
file.
development:
<<: *default
database: db/qonto_accounts.sqlite3
Then, create the SQLite database and its predefined tables with the following commands:
rails db:create
rails db:migrate
After running these commands, the db/schema.rb
file will be automatically created.
Start the application to receive HTTP calls:
rails s
Make a request like the following to create new transfers:
curl --location 'http://localhost:3000/transfers' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
"organization_name": "ACME Corp",
"organization_bic": "OIVUSCLQXXX",
"organization_iban": "FR10474608000002006107XXXXX",
"credit_transfers": [
{
"amount": "23.17",
"counterparty_name": "Bip Bip",
"counterparty_bic": "CRLYFRPPTOU",
"counterparty_iban": "EE383680981021245685",
"description": "Neverland/6318"
},
{
"amount": "982.34",
"counterparty_name": "Wile E Coyote",
"counterparty_bic": "ZDRPLBQI",
"counterparty_iban": "DE9935420810036209081725212",
"description": "//Spacex/AJGRBX/32"
},
{
"amount": "8024.99",
"counterparty_name": "Bugs Bunny",
"counterparty_bic": "RNJZNTMC",
"counterparty_iban": "FR0010009380540930414023042",
"description": "2020/DuckSeason/"
},
{
"amount": "200",
"counterparty_name": "Daffy Duck",
"counterparty_bic": "DDFCNLAM",
"counterparty_iban": "NL24ABNA5055036109",
"description": "2020/RabbitSeason/"
}
]
}'
To run the tests, just type:
rspec spec/