Skip to content

feat: release transfer service (Harish Algat)#116

Open
workharishalgat-lang wants to merge 1 commit into
Robustrade:mainfrom
workharishalgat-lang:solution/transfer-service
Open

feat: release transfer service (Harish Algat)#116
workharishalgat-lang wants to merge 1 commit into
Robustrade:mainfrom
workharishalgat-lang:solution/transfer-service

Conversation

@workharishalgat-lang

@workharishalgat-lang workharishalgat-lang commented Jul 12, 2026

Copy link
Copy Markdown

Summary

Implemented an idempotent wallet-to-wallet transfer service with atomic balance updates, durable transfer state, and double-entry ledger recording in PostgreSQL. The transfer flow is validated, transactionally executed, and safe under concurrent requests, including concurrent duplicate idempotency keys.

AI disclosure

I used AI as a development assistant for design review, implementation checks, and write-up drafting.

  1. What tool I used
  • GitHub Copilot Chat in VS Code
  • Model used by Copilot in this session: GPT-5.3-Codex
  1. How I generally use the tool
  • Clarify requirements and edge cases before coding.
  • Validate schema and transaction strategy against assignment goals.
  • Review service logic for idempotency and race conditions.
  • Draft and refine documentation/PR notes.
  • Run quick command guidance and troubleshooting when local environment issues occur.
  1. Transcript / prompts used
  • Full session transcript is available in the Copilot Chat history in VS Code.
  • If transcript export is not included, these are the prompts used in this session:
  1. I asked copilot to suggest strategies to handle all situations in distributed environment, asked all edge cases and the best practises that can be used.
  2. Add write set of unit tests to validate this functionality

Schema Design

Tables introduced:

  • wallets
  • transfers
  • ledger_entries
  • idempotency_records

Key constraints:

  • wallets.balance is constrained to be non-negative.
  • transfers.amount must be positive.
  • transfers.state is constrained to PENDING, PROCESSED, or FAILED.
  • transfers has a distinct-wallet constraint to prevent self-transfer.
  • ledger_entries.type is constrained to DEBIT or CREDIT.
  • ledger_entries.amount must be positive.
  • idempotency_records.idempotency_key is primary key.
  • Foreign keys connect transfers to wallets, ledger entries to transfers/wallets, and idempotency records to transfers.

Indexes:

  • transfers indexes on from_wallet_id, to_wallet_id, and state.
  • ledger_entries index on wallet_id.
  • Unique index on ledger_entries (transfer_id, type) to guarantee exactly one DEBIT and one CREDIT per transfer.

Idempotency Strategy

  • Each request includes an idempotency key.
  • On entry, the service checks idempotency storage:
  1. If key exists, it returns the stored original response snapshot.
  2. If key does not exist, it processes normally.
  • On both success and terminal business failure (for example, insufficient funds), the service stores the response snapshot under the key in the same transaction.
  • If concurrent duplicates race, one wins insert on the unique idempotency key; losers rollback and replay the winner’s stored response.
  • This ensures no duplicate side effects and stable retry behavior even after process restarts.

Concurrency Strategy

  • Entire transfer runs in one database transaction.
  • Both wallet rows are locked with SELECT FOR UPDATE.
  • Locks are acquired in deterministic sorted wallet-id order to prevent deadlocks for opposite-direction concurrent transfers.
  • Funds check is done while holding locks, preventing read-then-write races.
  • On success:
  1. Source wallet debited.
  2. Destination wallet credited.
  3. Exactly two ledger rows inserted (DEBIT and CREDIT).
  4. Transfer state updated to PROCESSED.
  5. Idempotency snapshot persisted.
  • On insufficient funds:
  1. Transfer state updated to FAILED.
  2. Failed response persisted under idempotency key for deterministic replay.
  • Database constraints provide a safety backstop against invalid states and negative balances.

How to Run

  • Option A (direct):
  1. export DATABASE_URL=postgres://user:pass@localhost:5432/wallet?sslmode=disable
  2. go run ./cmd/server
  • Option B (with local test environment):
  1. cd testenv
  2. ./run-local.sh

How to Test

  • Unit and default test run:
  1. go test ./...
  • Integration tests against real Postgres:
  1. export TEST_DATABASE_URL=postgres://wallet:wallet@localhost:5432/wallet?sslmode=disable
  2. go test ./test/integration/... -v

Tradeoffs / Assumptions

  • Amount is integer minor units (int64), no multi-currency support.
  • Wallet balance is stored for fast reads rather than fully derived each time from ledger history.
  • Failed transfer outcomes are also idempotently recorded, so the same key always returns the same result.
  • Schema creation is expected during DB provisioning; application runtime does not manage DDL.
  • Integration tests are opt-in via environment variable.

Checklist

  • Tests pass (blocked in this environment by dependency download/DNS to configured Go proxy)
  • Lint passes (blocked in this environment for the same dependency resolution reason)
  • Format check passes (gofmt check reported clean)
  • README or notes updated
  • PR description explains schema, idempotency, and concurrency

Copilot AI review requested due to automatic review settings July 12, 2026 17:17

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

This PR introduces a PostgreSQL-backed wallet-to-wallet transfer service with an idempotent POST /transfers API, transactional balance updates, and double-entry ledger recording, plus local/dev tooling and tests to validate concurrency and exactly-once behavior.

Changes:

  • Added transfer service orchestration (idempotency, wallet row locking, state transitions) and HTTP handler wiring.
  • Added Postgres persistence layer (repositories), domain models, and an authoritative SQL schema.
  • Added local Postgres test environment scripts and unit + real-Postgres concurrency integration tests; expanded README documentation.

Reviewed changes

Copilot reviewed 19 out of 21 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
cmd/server/main.go Service entrypoint wiring (config, DB, handler, graceful shutdown).
internal/api/handler.go HTTP POST /transfers endpoint and error mapping.
internal/config/config.go Environment-based configuration loading/validation.
internal/logging/logger.go Leveled logger utility used by service/runtime.
internal/model/model.go Domain models, enums, and ID generator.
internal/repository/repository.go Repository interfaces + shared error helpers.
internal/repository/postgres/wallet.go Postgres wallet persistence (row locking + balance updates).
internal/repository/postgres/transfer.go Postgres transfer persistence (create + state updates).
internal/repository/postgres/ledger.go Postgres ledger entry persistence.
internal/repository/postgres/idempotency.go Postgres idempotency record persistence.
internal/service/transfer_service.go Core transfer workflow: idempotency, locking, balance moves, ledger writes.
internal/service/transfer_service_test.go Unit tests for idempotency, snapshot semantics, and failure modes.
migrations/schema.sql Authoritative SQL DDL for wallets/transfers/ledger/idempotency.
test/integration/concurrency_test.go Real-Postgres concurrency/idempotency integration tests (gated by env).
testenv/docker-compose.yml Local Postgres container for manual/integration runs.
testenv/run-local.sh Script to start Postgres, wait for schema, seed wallets, run service.
testenv/seed.sql Seed wallets for local endpoint checks.
README.md Expanded documentation: architecture, schema, semantics, and how-to-run/test.
go.mod Go module definition and dependencies.
go.sum Dependency checksums.

Comment on lines +121 to +127
transfer := model.Transfer{
TransferID: model.NewID(),
FromWalletID: request.FromWalletID,
ToWalletID: request.ToWalletID,
Amount: request.Amount,
State: model.TransferStatePending,
}
"strings"

"github.com/Robustrade/wallet-transfer-assignment/internal/model"
"github.com/jackc/pgconn"
Comment thread testenv/seed.sql
Comment on lines +2 to +3
-- Run AFTER the service has started once, because the service creates the tables
-- via GORM AutoMigrate on startup.
Comment thread testenv/run-local.sh
set -euo pipefail
cd "$(dirname "$0")"

export DATABASE_URL="postgres://wallet:wallet@host.docker.internal:5432/wallet?sslmode=disable"
Comment thread internal/api/handler.go
Comment on lines +35 to +39
var request TransferRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
h.writeErrorStatus(w, http.StatusBadRequest, "invalid JSON payload")
return
}
Comment on lines +47 to +54
if err := db.AutoMigrate(
&model.Wallet{},
&model.Transfer{},
&model.LedgerEntry{},
&model.IdempotencyRecord{},
); err != nil {
t.Fatalf("automigrate: %v", err)
}
Comment thread internal/model/model.go
Comment on lines +71 to +73
var buffer [16]byte
_, _ = rand.Read(buffer[:])
return hex.EncodeToString(buffer[:])
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