✨ wallet transfer service design and implementation - #126
Open
Swapnil811 wants to merge 1 commit into
Open
Conversation
There was a problem hiding this comment.
Pull request overview
Implements a Go + PostgreSQL wallet-to-wallet transfer service with transactional balance updates, double-entry ledger writes, and API-level idempotency, plus docs and an integration test suite to validate concurrency/idempotency invariants.
Changes:
- Add PostgreSQL schema migrations for wallets, transfers, ledger entries, and idempotency records.
- Implement transfer workflow (transaction boundary, row locks, ledger writes, idempotency replay) with HTTP endpoint wiring.
- Add unit + PostgreSQL integration tests, plus README/Makefile to run the service and tests locally.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| wallet-transfer-service/README.md | Usage/run instructions and documented API/idempotency/testing behavior |
| wallet-transfer-service/Makefile | Targets for unit tests, race tests, and integration tests |
| wallet-transfer-service/docker-compose.yml | Local Postgres container definition for development/testing |
| wallet-transfer-service/migrations/001_create_wallets.sql | Create wallets table with non-negative balance constraint |
| wallet-transfer-service/migrations/002_create_transfers.sql | Create transfers table with basic invariants and indexes |
| wallet-transfer-service/migrations/003_create_ledger_entries.sql | Create ledger_entries table with debit/credit constraints + uniqueness |
| wallet-transfer-service/migrations/004_create_idempotency_records.sql | Create idempotency_records table for idempotency key + request hash |
| wallet-transfer-service/go.mod | Module definition and dependency set (uuid, pgx) |
| wallet-transfer-service/go.sum | Dependency checksums |
| wallet-transfer-service/cmd/server/main.go | Server bootstrap: DB pool, migrations, routes, health endpoint |
| wallet-transfer-service/internal/database/postgres.go | pgx pool creation and connectivity verification |
| wallet-transfer-service/internal/database/migrations.go | Minimal migration runner using schema_migrations bookkeeping |
| wallet-transfer-service/internal/domain/errors.go | Domain error definitions for handler/service mapping |
| wallet-transfer-service/internal/domain/wallet.go | Wallet domain model |
| wallet-transfer-service/internal/domain/transfer.go | Transfer domain model + status enum + request DTO |
| wallet-transfer-service/internal/domain/ledger.go | Ledger domain model + entry type enum |
| wallet-transfer-service/internal/repository/tx.go | DBTX abstraction for pool/tx reuse in repositories |
| wallet-transfer-service/internal/repository/repositories.go | Repository wiring container |
| wallet-transfer-service/internal/repository/wallet_repository.go | Wallet read-for-update + balance update operations |
| wallet-transfer-service/internal/repository/transfer_repository.go | Transfer persistence + status transition guard |
| wallet-transfer-service/internal/repository/ledger_repository.go | Ledger entry persistence + simple query helper |
| wallet-transfer-service/internal/repository/idempotency_repository.go | Idempotency record persistence + request-hash comparison |
| wallet-transfer-service/internal/service/transfer_service.go | Core transfer transaction workflow (locking, idempotency, ledger, status) |
| wallet-transfer-service/internal/service/transfer_service_test.go | Unit tests for request validation and hashing behavior |
| wallet-transfer-service/internal/handler/transfer_handler.go | POST /transfers JSON decode/validate + error/status mapping |
| wallet-transfer-service/internal/integration/transfer_integration_test.go | Postgres-backed tests for invariants + concurrency/idempotency scenarios |
| wallet-transfer-service/docs/DESIGN.md | Design rationale for schema, idempotency, concurrency/locking |
| wallet-transfer-service/docs/AI_USAGE.md | AI disclosure and prompt history |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+203
to
+210
| _, err := svc.CreateTransfer(ctx, domain.CreateTransferRequest{ | ||
| IdempotencyKey: "funds-" + uuid.NewString(), | ||
| FromWalletID: from, ToWalletID: to, Amount: 101, | ||
| }) | ||
| if err == nil { | ||
| t.Fatal("expected insufficient funds") | ||
| } | ||
|
|
Comment on lines
+18
to
+23
| const q = ` | ||
| INSERT INTO transfers | ||
| (id, from_wallet_id, to_wallet_id, amount, status, created_at, updated_at) | ||
| VALUES ($1, $2, $3, $4, $5, $6, $6)` | ||
|
|
||
| _, err := db.Exec(ctx, q, t.ID, t.FromWalletID, t.ToWalletID, t.Amount, t.Status, t.CreatedAt) |
Comment on lines
+58
to
+64
| if _, err := tx.Exec(ctx, `INSERT INTO schema_migrations(version) VALUES ($1)`, file); err != nil { | ||
| _ = tx.Rollback(ctx) | ||
| return fmt.Errorf("record migration %s: %w", file, err) | ||
| } | ||
| if err := tx.Commit(ctx); err != nil { | ||
| return fmt.Errorf("commit migration %s: %w", file, err) | ||
| } |
Comment on lines
+26
to
+43
| func setupDB(t *testing.T) *pgxpool.Pool { | ||
| t.Helper() | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
|
|
||
| pool, err := pgxpool.New(ctx, testDatabaseURL()) | ||
| if err != nil { | ||
| t.Skipf("PostgreSQL unavailable: %v", err) | ||
| } | ||
|
|
||
| if err := pool.Ping(ctx); err != nil { | ||
| pool.Close() | ||
| t.Skipf("PostgreSQL unavailable: %v", err) | ||
| } | ||
|
|
||
| return pool | ||
| } |
Comment on lines
+14
to
+17
| "github.com/Swapnil811/wallet-transfer-assignment/wallet-transfer-service/internal/domain" | ||
| "github.com/Swapnil811/wallet-transfer-assignment/wallet-transfer-service/internal/repository" | ||
| "github.com/Swapnil811/wallet-transfer-assignment/wallet-transfer-service/internal/service" | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Wallet Transfer Service supports wallet-to-wallet transfers with three core correctness requirements: balances must remain correct, concurrent requests must not cause double spending, and every successful transfer must have a balanced double-entry ledger. The design uses Go with PostgreSQL and separates HTTP handlers, business services, repositories, and domain models.
for schema, design and decisions, please check file wallet-transfer-service\docs\DESIGN.md
AI disclosure
Have added a file with all the prompts used for the design and implemention of the service. Please check wallet-transfer-service\docs\AI_USAGE.md
Schema Design
PostgreSQL is the source of truth for wallet balances, transfers, ledger entries, and idempotency records. The design uses four main tables.
| Table | Purpose | Important fields | Constraints / integrity |
| --- | --- | --- | --- |
| wallets | Current wallet state. | id, balance, created_at, updated_at | Primary key; balance must not be negative. |
| transfers | Business transfer and state machine. | id, from_wallet_id, to_wallet_id, amount, status, timestamps | Foreign keys; amount > 0; source != destination; status ∈ PENDING, PROCESSED, FAILED. |
| ledger_entries | Double-entry financial record. | id, transfer_id, wallet_id, type, amount, created_at | Foreign key; amount > 0; type ∈ DEBIT, CREDIT; uniqueness prevents duplicate entries. |
| idempotency_records | Maps client key to the original operation. | key, request_hash, transfer_id, created_at | Unique/primary-key idempotency key; request hash detects different payload reuse. |
| Decision / assumption | Rationale / tradeoff |
| --- | --- |
| Synchronous processing | The complete transfer is executed in one database transaction. There is no external payment provider or asynchronous workflow in scope. |
| PostgreSQL as source of truth | The solution relies on PostgreSQL transactions, constraints, uniqueness, and row-level locking. |
| Stored balance + ledger | Stored balance provides efficient reads; the ledger provides an audit trail. The transaction must update both consistently. |
| Integer money | Amounts use the smallest currency unit to avoid floating-point errors. Multi-currency behavior is out of scope. |
| No external side effects | There are no emails, webhooks, queues, or external payment calls, so a database transaction is sufficient for required side effects. |
| Idempotency retention | The assignment does not define expiry/retention. Records are retained unless a later operational policy is introduced. |
| API-level exactly-once | Exactly-once applies to the business operation when an idempotency key is used; it does not imply exactly-once network delivery. |
Idempotency Strategy
The API accepts an idempotencyKey so that clients can safely retry a request after a timeout or lost response. The key is stored in PostgreSQL and associated with the resulting transfer.
Concurrency Strategy
The principal concurrency risk is double spending. If a wallet has 10000 units and two concurrent requests each attempt to spend 10000, only one may succeed.
A transfer executes inside one PostgreSQL transaction:
Check/create the idempotency record.
Create the transfer in PENDING state.
Lock the source and destination wallet rows.
Check that the source has sufficient balance.
Debit the source and credit the destination.
Insert the DEBIT and CREDIT ledger entries.
Transition PENDING to PROCESSED.
Commit.
If any database operation fails before commit, the transaction rolls back. This prevents partial state such as a wallet debit without its corresponding credit or ledger entries.
How to Run
How to Test
Tradeoffs / Assumptions
Checklist