Add code for assignment - #18
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a minimal wallet-to-wallet transfer API backed by SQLite/GORM, aiming to provide idempotent, transactional, and double-entry transfers with a Dockerized runtime.
Changes:
- Introduces core domain models (wallets, transfers, ledger entries, idempotency records) and DB initialization/migration.
- Implements transfer creation flow (transaction, balance updates, ledger writes, idempotency storage) plus an HTTP handler and service-level tests.
- Adds containerization assets (Dockerfile + docker-compose) and updates repo docs/templates.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
internal/service/transfer_service.go |
Implements transfer logic, idempotency lookup/storage, wallet locking, and ledger writes |
internal/service/transfer_service_test.go |
Adds service tests for success, idempotency, balances, ledger, and concurrency |
internal/handler/handler.go |
Adds /transfers HTTP endpoint delegating to the service |
internal/domain/models.go |
Defines GORM models and constraints for wallet/transfer/ledger/idempotency |
internal/db/db.go |
Initializes SQLite connection and runs migrations |
cmd/main.go |
Wires DB/service/handler and provides basic health endpoint + seed wallets |
Dockerfile |
Multi-stage build for runtime container |
docker-compose.yml |
Runs the service and persists SQLite data via volume |
README.md |
Adds brief service feature/run/test notes |
.github/pull_request_template.md |
Expands template with solution-specific content |
go.mod / go.sum |
Adds GORM + SQLite + UUID dependencies |
| db, err := gorm.Open(sqlite.Open("/app/data/wallet.db"), &gorm.Config{}) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
|
|
||
| if err := db.Exec("PRAGMA foreign_keys = ON").Error; err != nil { | ||
| panic(err) | ||
| } |
| ## | ||
| # Wallet Service | ||
|
|
||
| ## Features | ||
| - Idempotent transfer API | ||
| - Double-entry ledger | ||
| - Transaction-safe | ||
| - Row-level locking | ||
| - Dockerized | ||
|
|
||
| ## Run | ||
| make run | ||
|
|
||
| ## Docker | ||
| make docker | ||
|
|
||
| ## Test | ||
| make test |
| if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). | ||
| First(&fromWallet, "id = ?", from).Error; err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). | ||
| First(&toWallet, "id = ?", to).Error; err != nil { | ||
| return err | ||
| } |
| err := s.db.Transaction(func(tx *gorm.DB) error { | ||
|
|
| func Init() *gorm.DB { | ||
| db, err := gorm.Open(sqlite.Open("/app/data/wallet.db"), &gorm.Config{}) | ||
| if err != nil { |
| # Ensure DB file path matches Go code | ||
| environment: | ||
| DB_PATH: /app/data/wallet.db | ||
|
|
| # Ensure executable | ||
| RUN chmod +x /app/app |
| if err != nil { | ||
| w.WriteHeader(http.StatusBadRequest) | ||
| if err := json.NewEncoder(w).Encode(map[string]string{ | ||
| "error": err.Error(), | ||
| }); err != nil { | ||
| log.Println("encode error:", err) | ||
| } | ||
| return |
| var existing domain.IdempotencyRecord | ||
| if err := s.db.First(&existing, "key = ?", key).Error; err == nil { | ||
| var resp map[string]string | ||
| if err := json.Unmarshal([]byte(existing.Response), &resp); err != nil { | ||
| return "", err | ||
| } | ||
| if id, ok := resp["transferId"]; ok { | ||
| return id, nil | ||
| } | ||
| return "", errors.New("invalid idempotency response") | ||
| } |
| if err := tx.Create(&domain.LedgerEntry{WalletID: from, Type: "DEBIT", Amount: amount, TransferID: transfer.ID}).Error; err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if err := tx.Create(&domain.LedgerEntry{WalletID: to, Type: "CREDIT", Amount: amount, TransferID: transfer.ID}).Error; err != nil { |
There was a problem hiding this comment.
Add explicit foreign key constraints (where supported) to prevent orphan records. Replace Type string in LedgerEntry with a typed enum (e.g., LedgerType) Consider adding CreatedAt / UpdatedAt for auditability
type LedgerType string
There was a problem hiding this comment.
Strengths
Correct use of transaction
Implements idempotency
Uses row-level locking → prevents race conditions
Double-entry ledger implemented correctly
Idempotency check is outside transaction → possible race condition
JSON unmarshal error must be handled
Missing validation for edge cases (empty IDs, same wallet)
💡 Suggested Fix
Move idempotency inside transaction for stronger guarantees.
There was a problem hiding this comment.
Strengths
Thin handler (good layering)
Delegates logic to service
Add HTTP method validation
Set Content-Type: application/json
Handle all Encode() errors (already improved 👍)
Consider request validation layer
💡 Improvement
w.Header().Set("Content-Type", "application/json")
📁 internal/middleware/logging.go (if added
Summary
Describe your solution briefly.
Built an idempotent wallet-to-wallet transfer service that ensures correctness under concurrent requests.
Idempotency: Each request includes an idempotencyKey. Duplicate requests return the same stored response, preventing duplicate transfers.
Atomicity: Transfers run inside a database transaction, ensuring all operations succeed or fail together.
Concurrency Safety: Uses row-level locking (FOR UPDATE) to prevent race conditions and double spending.
Financial Correctness: Implements double-entry ledger (DEBIT + CREDIT) so total debit equals total credit.
Validation: Ensures valid inputs and sufficient balance before processing.
Testing: Covers success, failure, idempotency, and concurrent scenarios.
Detail how you used AI to help with your submission (including the tools you used, how
you used them and what your prompts were).
Include these points in detail
What tool you used (Cursor, Claude Code, Antigratvity etc.)
How you generally use the tool for your work.
A transcript of your entire session with your AI tool of choice. You can add this to the repo or email it to us with your submission. If for some reason, this is not possible, give us all the prompts that you used with the AI.
go fmt ./...
internal\db\db.go
internal\domain\models.go
internal\service\transfer_service.go
internal\service\transfer_service_test.go
golangci-lint run --build-tags "sqlite"
internal\service\transfer_service_test.go:113:16: Error return value of
json.Unmarshalis not checked (errcheck)json.Unmarshal([]byte(record.Response), &resp)
^
internal\service\transfer_service_test.go:130:22: Error return value of
svc.CreateTransferis not checked (errcheck)svc.CreateTransfer(context.Background(), string(rune(i)), "w1", "w2", 5)
^
cmd\main.go:24:16: Error return value of
w.Writeis not checked (errcheck)w.Write([]byte("OK"))
^
cmd\main.go:27:24: Error return value of
http.ListenAndServeis not checked (errcheck)http.ListenAndServe(":8080", nil)
Schema Design
Describe the tables, constraints, and indexes you introduced.
The system uses four core tables:
Wallet
Stores wallet balances
Fields:
id (primary key)
balance (constraint: balance >= 0)
Indexes:
Primary key on id
Purpose:
Represents user accounts and ensures balances never go negative
Represents each transfer transaction
Fields:
Purpose:
Tracks transfer metadata
Stores financial records for auditability
Fields:
Purpose:
Ensures financial correctness:
Every transfer creates:
1 DEBIT entry
1 CREDIT entry
Guarantees: total debit = total credit
IdempotencyRecord
Ensures safe retries
Fields:
key (primary key, unique)
response
Indexes:
Primary key on key
Purpose:
Stores request result for replay
Idempotency Strategy
🔁 Idempotency Strategy
Each API request includes an idempotencyKey
Flow:
Check if key exists in IdempotencyRecord
If exists:
Deserialize stored response
Return same transferId (no re-execution)
If not:
Execute transfer
Store response in DB
Return result
Guarantees:
Prevents duplicate transfers
Safe retries under network failures
Provides exactly-once behavior at API level
Concurrency Strategy
Explain how you prevent race conditions and double spending.
To prevent race conditions and double spending:
Database Transactions
Entire transfer runs inside a single transaction
Ensures:
atomic updates
all-or-nothing execution
Row-Level Locking
Uses SELECT ... FOR UPDATE (via GORM locking clause)
Locks both wallets during transfer
Prevents concurrent updates on same wallets
Balance Validation
Balance checked inside transaction
Ensures consistency under concurrent requests
Idempotency + Transaction Combined
Prevents duplicate execution even under retries
How to Run
docker-compose up --build
go run ./cmd/main.go
How to Test
Tradeoffs / Assumptions
Tradeoff: Transfers are processed synchronously.
Checklist
🧠 Design Overview
This system implements an idempotent wallet-to-wallet transfer service ensuring correctness under concurrent requests.
Key Features
Idempotency
Transaction Safety
Concurrency Control
FOR UPDATE)Double-entry Ledger