Skip to content

✨ wallet transfer service design and implementation - #126

Open
Swapnil811 wants to merge 1 commit into
Robustrade:mainfrom
Swapnil811:solution/swapnil-suryawanshi
Open

✨ wallet transfer service design and implementation#126
Swapnil811 wants to merge 1 commit into
Robustrade:mainfrom
Swapnil811:solution/swapnil-suryawanshi

Conversation

@Swapnil811

Copy link
Copy Markdown

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

  1. I used Chatgpt and cursor (composer 2.5 fast model)
  2. How you generally use the tool for your work. - Yes I use those extensively, our company encourages to use AI and speed up the delivery
  3. please check the file for the prompts

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:

  1. Check/create the idempotency record.

  2. Create the transfer in PENDING state.

  3. Lock the source and destination wallet rows.

  4. Check that the source has sufficient balance.

  5. Debit the source and credit the destination.

  6. Insert the DEBIT and CREDIT ledger entries.

  7. Transition PENDING to PROCESSED.

  8. 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

  • Tests pass
  • Lint passes
  • Format check passes
  • README or notes updated
  • PR description explains schema, idempotency, and concurrency

Copilot AI lite review requested due to automatic review settings August 12, 2026 04:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants