{"id":22737222,"url":"https://github.com/hhow09/simple_bank","last_synced_at":"2026-02-13T19:31:40.741Z","repository":{"id":46267966,"uuid":"412710649","full_name":"hhow09/simple_bank","owner":"hhow09","description":"simple bank implementation with Golang, PostgreSQL, SQLC","archived":false,"fork":false,"pushed_at":"2025-01-30T11:32:18.000Z","size":389,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-08-22T14:54:11.341Z","etag":null,"topics":["docker-compose","go","go-fx","golang","jwt","postgresql","sqlc"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hhow09.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-10-02T06:25:57.000Z","updated_at":"2025-01-30T11:32:22.000Z","dependencies_parsed_at":"2023-10-20T16:46:03.273Z","dependency_job_id":"2251a1bb-80c7-4e6e-bef4-54c57a4418e3","html_url":"https://github.com/hhow09/simple_bank","commit_stats":{"total_commits":44,"total_committers":2,"mean_commits":22.0,"dds":"0.045454545454545414","last_synced_commit":"c24c49e95770b859b9312e84b14e238a2cd10475"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hhow09/simple_bank","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hhow09%2Fsimple_bank","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hhow09%2Fsimple_bank/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hhow09%2Fsimple_bank/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hhow09%2Fsimple_bank/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hhow09","download_url":"https://codeload.github.com/hhow09/simple_bank/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hhow09%2Fsimple_bank/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29415560,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-13T06:24:03.484Z","status":"ssl_error","status_checked_at":"2026-02-13T06:23:12.830Z","response_time":78,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker-compose","go","go-fx","golang","jwt","postgresql","sqlc"],"created_at":"2024-12-10T22:10:29.769Z","updated_at":"2026-02-13T19:31:40.722Z","avatar_url":"https://github.com/hhow09.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Simple Bank\n![coverage](https://raw.githubusercontent.com/hhow09/simple_bank/refs/heads/badges/.badges/master/coverage.svg)\n\na simple bank service built with Golang (`gin`), `PostgreSQL`, [sqlc](https://github.com/sqlc-dev/sqlc).\n\n## Introduction\n- Easy to maintain, preferment also type-safe code for query generated from [sqlc](https://github.com/sqlc-dev/sqlc).\n- Structured and dependency injection with [uber-go/fx](https://github.com/uber-go/fx)\n- Test-driven development style with high test coverage using `golang/mock`\n- Token-based authentication using [PASETO](https://github.com/paragonie/paseto).\n- Containerized service, easy to run with [docker-compose](https://docs.docker.com/compose/)\n- use env var file for config with [Viper](https://github.com/spf13/viper)\n- RESTful API with auto generated api doc with [swaggo/swag](https://github.com/swaggo/swag)\n\n## Functions\n- User can create a `User` based on unique `username` and `email`.\n- A log-in `User` can create multiple accounts with different currencies.\n- Record all account balance changes in `Entry` table. Whenever some money is added to or subtracted from the account, an account entry record will be created.\n- `/transfer` api, provide a money transfer function between 2 accounts. This happen **within a transaction** and transfer is thread-safe operation.\n\n## Start the service\n### Build and run the service\n```bash\ndocker-compose up --force-recreate --build api\n```\n\n### Play Manually with Postman\n1. check [http://localhost:8080/swagger/index.html](http://localhost:8080/swagger/index.html) for API doc\n2. install [Postman](https://www.postman.com/)\n3. import [postman-cmds.json](./postman-cmds.json)\n4. have fun\n    1. create user\n    2. login\n    3. JWT header: after login, copy the `access_token` in response and update variable the `auth header` with `bearer {access_token}`\n\n## Database Schema\n![](./db/dbdiagram.png)\n\n\n## Setup local development\n\n### Run Local Infra with dev server\n```bash\ndocker-compose -f docker-compose.infra.yaml up -d\ngo run main.go\n```\n\n### Install tools\n- [Docker desktop](https://www.docker.com/products/docker-desktop)\n- [TablePlus](https://tableplus.com/)\n- [Golang](https://golang.org/)\n- [Migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate)\n    - if you want to create migration e.g. [step 15](#15-add-users-table-with-unique--foreign-key-constraints-in-postgresql)\n- [Sqlc](https://github.com/kyleconroy/sqlc#installation)\n    - if you want to generate go code from sql\n- [Gomock](https://github.com/golang/mock)\n    - if you want to generate mock code\n\n### How to generate code\n\n- Generate SQL CRUD with sqlc:\n\n    ```bash\n    make sqlc\n    ```\n\n- Generate DB mock with gomock:\n\n    ```bash\n    make mock\n    ```\n\n- Create a new db migration:\n\n    ```bash\n    migrate create -ext sql -dir db/migration -seq \u003cmigration_name\u003e\n    ```\n\n---\n\n## Dev Progress\n### 1. Setup local environment\n\n### 2. Design [dbdiagram](./db/dbdiagram) with https://dbdiagram.io/\n- Foreign Key: `ref: \u003e A.id`, \n- Timestamp Type: `timestamptz`\n- Generate sql [000001_init_schema.up.sql](./db/migtation/000001_init_schema.up.sql)\n\n### 3. Setup Postgres with Docker and DB Migration\n```bash\ndocker-compose -f docker-compose.infra.yaml up -d\ndocker exec -it \u003cCONTAINER_ID\u003e psql -d simple_bank -U root\n```\n- now we should be able to see tables created by migration script\n- we can also connect DB with [TablePlus](https://tableplus.com/)\n\n### 4. Generate CRUD Golang code from SQL\n- Write CRUD SQL query in [db/query](./db/query)\n- generate golang code with `make sqlc`\n- init go module `go mod init github.com/hhow09/simple_bank`\n\n### 5. Write Golang unit tests for database CRUD with random data\n- Write tests\n    - [main_test.go](./db/sqlc/main_test.go): to make db connection\n    - use `testQueries` to access functions in `[query].sql.go`\n    - write following tests\n    - [account_test.go](./db/sqlc/account_test.go)\n    - [entry_test.go](./db/sqlc/entry_test.go)\n    - [transfer_test.go](./db/sqlc/transfer_test.go)\n- `make test`\n- go [context](https://pkg.go.dev/context): carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.\n\n### 6. implement database transaction in Golang\n- Create [store.go](./db/sqlc/store.go)\n    - `Store`: provides all funcs to execute queries and transactions\n    - `execTx`: define a private transaction function: begin -\u003e Commit or Rollback\n    - `TransferTx`: define a public transfer transaction function\n        1. create transfer record\n\t    2. create Entry of from_account\n\t    3. create Entry of to_account\n        4. update accounts' balance\n- Write [store_test.go](./db/sqlc/store_test.go)\n    - create 5 goroutine to test transaction\n    - get the err and result with go [channel](https://tour.golang.org/concurrency/2)\n### 7. Handle Transaction Lock\n- Now transfer transaction will not pass the test since\n    - `GetAccount` SQL is `SELECT` and does not block each other\n    - it will result in all concurrent `GetAccount` just return initial value\n- Create a SQL that `SELECT FOR UPDATE`\n    ```\n    -- name: GetAccountForUpdate :one\n    SELECT * FROM accounts\n    WHERE id = $1 LIMIT 1;\n    FOR UPDATE\n    ```\n    \n#### Deadlock\n```sql\nBEGIN;\nINSERT INTO transfers (from_account_id, to_account_id, amount) VALUES (1, 2, 10) RETURNING *: -- exclusive lock on accounts\n\nINSERT INTO entries (account_id, amount) VALUES (1, -10) RETURNING\nINSERT INTO entries (account_id, amount) VALUES (2, 10) RETURNING *;\nSELECT * FROM accounts WHERE id = 1 FOR UPDATE;\nUPDATE accounts SET balance = 90 WHERE id = 1 RETURNING *;\n\nSELECT * FROM accounts WHERE id = 2 FOR UPDATE; -- exclusive lock on accounts\nUPDATE accounts SET balance = 110 WHERE id = 2 RETURNING *;\nCOMMIT;\n```\n- since `transfers` Table has foreign key `from_account_id` and `to_account_id` referencing `accounts` Table\n- `INSERT INTO transfers` will acquire a `RowExclusiveLock` on accounts Table to ensure that ID of accounts are not consistent.\n- `SELECT * FROM accounts ... FOR UPDATE` will also acquire a lock on accounts\n\n### Deadlock happens\n| TX1                                   \t| TX2                                   \t|                                    \t|\n|---------------------------------------\t|---------------------------------------\t|------------------------------------\t|\n| BEWGIN                                \t|                                       \t|                                    \t|\n|                                       \t| BEGIN                                 \t|                                    \t|\n|                                       \t| INSERT INTO TRANSFERS                 \t| TX2 lock on account table          \t|\n|                                       \t| INSERT INTO ENTRIES                   \t|                                    \t|\n|                                       \t| INSERT INTO ENTRIES                   \t|                                    \t|\n| INSERT INTO TRANSFERS                 \t|                                       \t| TX1 lock on account table          \t|\n|                                       \t| SELECT * FROM accounts ... FOR UPDATE \t| waiting lock from TX1              \t|\n| INSERT INTO ENTRIES                   \t|                                       \t|                                    \t|\n| INSERT INTO ENTRIES                   \t|                                       \t|                                    \t|\n| SELECT * FROM accounts ... FOR UPDATE \t|                                       \t| waiting lock from TX2\u003cbr\u003edeadlock! \t|\n\n### Solution\n- we are only update the `balance` of account. The lock is unneeded.\n- change: `FOR UPDATE` -\u003e `FOR NO KEY UPDATE`\n- Refactor \n    - `getAccountForUpdate`+`UpdateAccount` = `AddAccountBalance`\n\n\n### 8. Avoid DeadLock\n- We will encounter deadlock when 2 transactions: `acc1` -\u003e `acc2` and `acc2` -\u003e `acc1` are running concurrently.\n```sql\n-- gorutine 1: transfer from id=1 to id=2\nBEGIN;\nUPDATE accounts SET balance = balance - 10 WHERE id = 1 RETURNING *;\nUPDATE accounts SET balance = balance + 10 WHERE id = 2 RETURNING *; \nCOMMIT; \n\n-- gorutine 2: transfer from id=2 to id=1\nBEGIN;\nUPDATE accounts SET balance = balance - 10 WHERE id = 2 RETURNING *; \nUPDATE accounts SET balance = balance + 10 WHERE id = 1 RETURNING *;\nCOMMIT; \n```\n\n- However if we switch the order so that **transactions always acquire locks in a consistent order**\n```golang\nif arg.FromAccountID \u003c arg.ToAccountID {\n\tresult.FromAccount, result.ToAccount, err = addMoney(ctx, q, arg.FromAccountID, -arg.Amount, arg.ToAccountID, arg.Amount)\n} else {\n\tresult.ToAccount, result.FromAccount, err = addMoney(ctx, q, arg.ToAccountID, arg.Amount, arg.FromAccountID, -arg.Amount)\n}\n```\n\nthe deadlock will not happen.\n- we can test with `TestTransferTxDeadlock`\n\n### 9. Isolation levels \u0026 read phenomena in MySQL \u0026 PostgreSQL \n\n#### MySQL\n```sql\nSELECT @@transaction_isolation --isolation level of current session\nSELECT @@global.transaction_isolation --isolation level of global session\n```\n\n#### PostgreSQL\n- only has 3 isolation level since `read uncommitted` is actually `read committed`\n- transaction isolation can only be set in one transaction.\n```sql\nshow transaction isolation level;\n```\n#### repeatable read is different in MySQL and PostgreSQL\n- in isolation level `repeatable read`\n\n```\nTable accounts\n id  | owner  | balance | currency |          \n-----+--------+---------+----------+\n   1 | tom    |     100 | USD      |\n   2 | mary   |     100 | USD      |\n```\n\n```\nSteps:\n1. process A, select * from accounts: tom's balance = 100\n2. process B, select * from accounts where id=1: tom's balance = 100\n3. process A, update accounts set balance = balance - 10 where id=1 returning *; tom's balance = 90\n4. process B, select * from accounts where balance \u003e=100: tom will appear since tom's balance = 100, (repeatable read)\n5. process A, commit; \n6. process B, update accounts set balance = balance - 10 where id=1 returning *; \n7. process B, commit;\n```\n\n- running those steps in MySQL:\n    - after step 7 we will get tom's balance: 80\n    - however it does not make sense since we expect tom's balance to be 100\n- running those steps in MySQL:\n    - after step 6 we will get `ERROR:  could not serialize access due to concurrent update`\n\n#### MySQL v.s. PostgreSQL\n|                         | MySQL         | PostgreSQL           |\n|-------------------------|---------------|----------------------|\n| isolation levels        | 4             | 3                    |\n| mechanism               | lock          | dependency detection |\n| default isolation level | read commited | repeatable read      |\n\n#### Reference\n- [MySQL - 15.7.2.1 Transaction Isolation Levels](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html)\n- [PostgreSQL - 13.2. Transaction Isolation](https://www.postgresql.org/docs/current/transaction-iso.html)\n\n### 10. Github Action For unit test\n- [.github/workflows/test.yml](./.github/workflows/test.yml)\n\n### 11. Implement RESTful HTTP API in Go using Gin\n- install [gin-gonic/gin](https://github.com/gin-gonic/gin) for http router and implement REST API\n- in [api](./api)\n    - define routing and handlers\n    - use [Model binding and validation](https://github.com/gin-gonic/gin#model-binding-and-validation) to do param validation.\n\n### 12. Load config from file \u0026 environment variables in Golang with [Viper](https://github.com/spf13/viper)\n- default config [app.env](./app.env)\n- env var can be further override with `environment` in [docker-compose.yaml](./docker-compose.yaml) \u003e\n\n### 13. Mock DB for testing HTTP API in Go and achieve 100% coverage\n- We need a mockDB\n#### Currently Server is using `store` of real db\n```golang\ntype Server struct {\n\tstore  *db.Store\n    ...\n}\n```\n- we can generate `Querier` interface by setting`emit_interface: true`\n    - `var _ Querier = (*Queries)(nil)` means `Queries` struct must implement `Querier` interface\n- we can make a higher level abstraction `Store` interface\n    - we can embed `Querier` interface in to `Store` to ensure it has all \n    - `SQLStore` must implements `Store`\n- `make mock`\n    - package name: `mockdb`\n    - destination `db/mock/store.go` \n    - Interface to mock: `Store`\n- write tests in [account_test.go](./api/account_test.go) using `store *mockdb.MockStore`\n    - use [httptest.NewRecorder](https://pkg.go.dev/net/http/httptest#NewRecorder) and `router.ServeHTTP` to write test and record them.\n    - achieve 100%\n\n### 14.  Implement transfer money API with a custom params validator\n- implement `POST /transfers` API\n    - instead of hard code the available currency in param binding, register a custom currency validator to gin\n    - use `ShouldBindJSON` in `*gin.Context` to parse JSON\n    - validate requested transfer currency should match two accounts\n\n### 15. Add users table with unique \u0026 foreign key constraints in PostgreSQL\n- now we still missing user authentication and authorization, we need user Tables.\n- modify [dbdiagram](./db/dbdiagram) to add user Table, some db constraints:\n    - email should be `unique`\n    - a user can have multiple account: `accounts.owner` should be the foreign key of username.\n    - an owner cannot own two account with same currency: \n        - `(owner, currency) [unique]`\n        - `ALTER TABLE \"accounts\" ADD CONSTRAINT \"owner_currency_key\" UNIQUE (\"owner\", \"currency\");`\n- generate postgreSQL script from https://dbdiagram.io/\n- `migrate create -ext sql -dir db/migration -seq add_users` to add a new migration script, [000002_add_users.up.sql](./db/migration/000002_add_users.up.sql)\n- `make migratedown`\n\n### 16. How to handle DB errors in Golang correctly\n- `make sqlc` to generate updated model and `*.sql.go` \n- write unit test [user_test.go](./db/sqlc/user_test.go)\n- handle db constraints violation in `CreateAccount`\n    - `foreign_key_violation`: an account should only be created **when owner is an existing user**\n    - `unique_violation`: an `owner-currency` index should be unique\n    - both of them should return `http.StatusForbidden` (403)\n\n### 17. How to securely store passwords? Hash password in Go with Bcrypt!\n- use [bcrypt](https://pkg.go.dev/golang.org/x/crypto/bcrypt) to do password hashing and checking in [password.go](./util/password.go)\n    - bcrypt will generate hashed password with `cost` `random salt` \n    - if we hash same password twice, output should be different\n- implement `createUser` handler in [user.go](./api/user.go).\n    - check [go-playground/validator - Baked-in Validations](https://github.com/go-playground/validator#baked-in-validations).\n\n### 18. How to write stronger unit tests with a custom gomock matcher\n- [user_test.go](./api/user_test.go)\n    - first `randomUser()` to generate a random `user` as input\n    - compare the input `user` with API result, the password part should be checked with custom matcher since hashing 2 times will generate different result.\n\n### 19. [PASETO](https://github.com/paragonie/paseto) better than JWT\n#### JWT\n- composed of: `base64(Header)` + `base64(Payload)` + `base64(Signature)`\n- for local use\n    - symmetric digital signature algorithm: use same secrete key to sign and verify\n- for public use\n    - asymmetric digital signature algorithm: use private key to sign token and public key to verify token.\n- cons: some algorithms are known to be vulnerable\n- cons: prone to **trivial token forgery**: \n    - Send a header that specifies the `none` algorithm be used\n    - Send a header that specifies the `HS256` algorithm when the application normally signs messages with an RSA public key.\n#### PASETO\n- [A Thorough Introduction to PASETO](https://developer.okta.com/blog/2019/10/17/a-thorough-introduction-to-paseto)\n- similar to JWT\n- for local use\n    - symmetric encryption: use same secrete key to sign and verify\n    - composed of: `Version` + `local` + `encrypted(Payload)` + `base64(Footer)`\n    - Payload: `data` + `expiration date` + `Nonce` + `Auth Tag`\n- for Public use\n    - symmetric encryption: use same secrete key to sign and verify\n    - composed of: `Version` + `public` + `base64(signed string)` \n- Pros: Stronger algorithms than JWT.\n- Pros: No trivial forgery since you don't need to choose algorithm.\n- Pros: local `Payload` is encrypted, there won’t be any way for attackers to see any of your payload data without your secret key.\n\n### 20. How to create and verify JWT \u0026 PASETO token in Golang\n- [token](./token) package\n\n### 21. Implement login user API that returns PASETO or JWT access token in Go\n- add config of `TOKEN_SYMMETRIC_KEY` and `ACCESS_TOKEN_DURATION` as server config\n- setup [loginUser](./api/user.go) API\n- write test cases in [user_test.go](./api/user_test.go)\n\n### 22. Implement authentication middleware and authorization rules in Golang using Gin\n- setup [middleware.go](./api/middleware.go) and [middleware_test.go](./api/middleware_test.go) to setup token verification for routes\n- setup `router.Group(\"/\").Use(authMiddleware(server.tokenMaker))` in [server.go](./api/server.go)\n- update the auth rules for apis\n    - `createUser`: public\n    - `CreateAccount`: auth middleware, a user can only create accounts it owns.\n    - `getAccount`: auth middleware, can only see accounts created by the request user itself.\n    - `listAccounts`: auth middleware, can only see accounts created by the request user itself.\n        - add `WHERE owner = $1` for listAccount Query\n    - `CreateTransfer`: auth middleware, fromAccount should be the account owned by user itself.\n- update unit test with `setupAuth` \n\n### 23. Build a minimal Golang Docker image with a multistage Dockerfile\ntest the built simple_bank_api container in dev environment \n```bash\ndocker-compose -f docker-compose.infra.yaml up -d\ndocker-compose -f docker-compose.server-only.yaml up --force-recreate --build api\n```\n\n### 24. Use docker network to connect 2 stand-alone containers\n- solved in `docker-compose.server-only.yaml`\n\n### 25. write [docker-compose.yaml](docker-compose.yaml)\n1. start db\n2. run migration\n3. start service\n\n### 26. Generate API Doc with [swaggo/swag](https://github.com/swaggo/swag)\n- `make swagger` then swagger files will be in [docs](./docs)\n- run the app\n- check http://localhost:8080/swagger/index.html\n\n### 26. Dependency Injection using [uber-go/fx](https://github.com/uber-go/fx)\n- reference: [dipeshdulal/clean-gin](https://github.com/dipeshdulal/clean-gin)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhhow09%2Fsimple_bank","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhhow09%2Fsimple_bank","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhhow09%2Fsimple_bank/lists"}