https://github.com/0xkalvin/transfer-service
A completely over-engineered, Node.js bank transfer service that ~~tries to~~ leverages hexagonal architecture.
https://github.com/0xkalvin/transfer-service
aws banking grpc hexagonal-architecture nodejs repl sqs transfer
Last synced: over 1 year ago
JSON representation
A completely over-engineered, Node.js bank transfer service that ~~tries to~~ leverages hexagonal architecture.
- Host: GitHub
- URL: https://github.com/0xkalvin/transfer-service
- Owner: 0xkalvin
- Created: 2021-08-02T16:16:49.000Z (almost 5 years ago)
- Default Branch: main
- Last Pushed: 2021-08-19T20:04:55.000Z (almost 5 years ago)
- Last Synced: 2025-03-27T14:55:45.876Z (over 1 year ago)
- Topics: aws, banking, grpc, hexagonal-architecture, nodejs, repl, sqs, transfer
- Language: JavaScript
- Homepage:
- Size: 462 KB
- Stars: 6
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Transfer service
A simple, over-engineered Node.js bank transfer system that leverages ~~or tries to~~ hexagonal architecture. It's an in progress project to play around and have fun, feel free to contribute by any means.
## Requirements
- Must support creating accounts and transfers.
- Must support creating transfer through a REST API, a SQS queue, a GRPC API, and also a REPL. That's what "Transporters" are, alternative ways of inputting data in this application for interacting with the business entities.
- When a transfer is processed, accounts' balances should be updated.
- Must use a couple of databases and messaging queues for the sweet purpose of over engineering :joy:
## Architecture
- Two servers: one using REST and one using GRPC.
- Two queue workers: one consuming a SQS queue and one using Kafka.
- Databases: Postgres for transactional data, Elasticsearch for fast reads/reporting, and Redis for ephemeral data such as Idempotency keys.
- Each Idempotency key used for creating a transfer should cease to exist after 24 hours.
- When a transfer created, it is persisted on a Postgres's table, replicated to an Elasticseach storage
, and its Idempotency key is inserted in Redis Layer for 24 hours of timespan.
- When a Transfer is created, it must be enqueued in a Kafka topic so it can be processed in background async.
## Folder structure
```text
src
├── data-sources
│ ├── elasticsearch
│ │ ├── config.js
│ │ └── index.js
│ ├── kafka
│ │ ├── client.js
│ │ ├── config.js
│ │ ├── consumer.js
│ │ ├── index.js
│ │ └── producer.js
│ ├── postgres
│ │ ├── config.js
│ │ ├── index.js
│ │ ├── migrations
│ │ │ ├── 20210803012952-create-accounts.js
│ │ │ └── 20210803013010-create-transfers.js
│ │ └── models
│ │ ├── account.js
│ │ ├── index.js
│ │ └── transfer.js
│ ├── redis
│ │ ├── config.js
│ │ └── index.js
│ └── sqs
│ ├── client.js
│ ├── config.js
│ ├── index.js
│ ├── local-setup.conf
│ └── poller.js
├── lib
│ ├── business-errors.js
│ └── logger.js
├── repositories
│ ├── account
│ │ └── index.js
│ └── transfer
│ └── index.js
├── services
│ ├── account
│ │ └── index.js
│ └── transfer
│ └── index.js
└── transporters
├── grpc
│ ├── controllers
│ │ ├── account.js
│ │ └── transfer.js
│ ├── entrypoint.js
│ └── proto
│ ├── account.proto
│ ├── shared.proto
│ └── transfer.proto
├── kafka
│ └── entrypoint.js
├── repl
│ └── entrypoint.js
├── rest
│ ├── application.js
│ ├── controllers
│ │ ├── account
│ │ │ └── index.js
│ │ └── transfer
│ │ └── index.js
│ ├── entrypoint.js
│ ├── http-errors.js
│ └── middlewares
│ ├── error-handler.js
│ ├── http-logger.js
│ ├── validator-handler.js
│ └── wrap-async.js
└── sqs
├── entrypoint.js
├── process.js
└── processors
├── index.js
└── transfers
└── index.js
```
## Running locally
Requirements:
- Docker
- Docker compose
- Unix-like OS
After fulling all requirements, start everything up by running
```sh
make
```
Then you will find running:
- a Http server listening on port 3000
- Postgres + Elasticsearch + redis containers
- kafka + zookeeper + sqs containers
Check out the api reference below for details about how to call each endpoint :)
## REPL
To start up the repl, just run
```sh
make repl
```
In the repl, you can also create a transfer as following
```js
await createTransfer({ sourceAccountId: "f0916334-e4eb-43cb-b9eb-a2a9e89277e3", targetAccountId: "b4703965-b530-4941-b6e0-c975dce98ac9", transferAmount: 15 })
```
## REST API
### POST /accounts
Request
```sh
curl -XPOST http://localhost:3000/accounts \
--header 'Content-Type: application/json' \
--data '{
"amount": 1,
"source_account_id": "acc_ckqe3n57o00040jtl4yuz2jh5",
"target_account_id": "acc_ckqe3n83w00070jtl1tz52p8g"
}'
```
Response (status code 201)
```json
{
"id": "45ba22be-ac12-4693-9e21-e7ba3ca279d0",
"balance": 100000,
"holder_name": "Bojack Horseman",
"holder_document_number": "12345678910"
}
```
### GET /accounts/:id
Request
```sh
curl http://localhost:3000/accounts/1
```
Response
```json
{
"id": "45ba22be-ac12-4693-9e21-e7ba3ca279d0",
"balance": 100000,
"holder_name": "Bojack Horseman",
"holder_document_number": "12345678910"
}
```
### GET /accounts/:id/transfers
Request
```sh
curl http://localhost:3000/accounts/1/transfers
```
Response
```json
[
{
"id": "ce4ae758-cf95-412a-a603-275fd22f5991",
"amount": 1,
"source_account_id": "acc_ckqe3n57o00040jtl4yuz2jh5",
"target_account_id": "acc_ckqe3n83w00070jtl1tz52p8g",
"status": "failed"
},
{
"id": "ce4ae758-cf95-412a-a603-275fd22f5992",
"amount": 1,
"source_account_id": "acc_ckqe3n57o00040jtl4yuz2jh5",
"target_account_id": "acc_ckqe3n83w00070jtl1tz52p8g",
"status": "pending"
}
{
"id": "ce4ae758-cf95-412a-a603-275fd22f5993",
"amount": 1,
"source_account_id": "acc_ckqe3n57o00040jtl4yuz2jh5",
"target_account_id": "acc_ckqe3n83w00070jtl1tz52p8g",
"status": "settled"
}
]
```
### POST /transfers
Request
```sh
curl -XPOST http://localhost:3000/transfers \
--header 'Content-Type: application/json' \
--data '{
"amount": 1,
"source_account_id": "acc_ckqe3n57o00040jtl4yuz2jh5",
"target_account_id": "acc_ckqe3n83w00070jtl1tz52p8g"
}'
```
Response (status code 201)
```json
{
"id": "ce4ae758-cf95-412a-a603-275fd22f599f",
"amount": 1,
"source_account_id": "acc_ckqe3n57o00040jtl4yuz2jh5",
"target_account_id": "acc_ckqe3n83w00070jtl1tz52p8g",
"status": "pending"
}
```
## TODO
- decouple Kafka transporter
- Add database transations
- Add schemas