Skip to content

Solution/prasenjit - #117

Open
Prasen1 wants to merge 9 commits into
Robustrade:mainfrom
Prasen1:solution/prasenjit
Open

Solution/prasenjit#117
Prasen1 wants to merge 9 commits into
Robustrade:mainfrom
Prasen1:solution/prasenjit

Conversation

@Prasen1

@Prasen1 Prasen1 commented Jul 19, 2026

Copy link
Copy Markdown

Summary

Go implementation of the wallet transfer assignment. The service supports
idempotent wallet-to-wallet transfers, stored balances, and a double-entry ledger.

AI disclosure

Used Claude Sonnet for researching and planning the design of the project based on requirements and then passed the spec to Codex for creating the implementation plan (attached). From there, the prompts were just to implement the project step by step with my review at every step.

At work, I use Claude code (Opus for research and design and Sonnet for execution).

Schema Design

The implementation introduces a normalized PostgreSQL schema that separates wallet state, transfer intent, idempotency tracking, and accounting history.

Tables

  • wallets

    • Stores each wallet’s identity, owner, currency, opening balance, current balance, status, and optimistic version.
    • Constraints:
      • PRIMARY KEY on id
      • CHECK (opening_balance >= 0)
      • CHECK (balance >= 0)
      • CHECK (status IN ('ACTIVE', 'FROZEN', 'CLOSED'))
    • Index:
      • ix_wallets_owner on owner_id
  • idempotency_records

    • Tracks a unique idempotency key, the request fingerprint, the linked transfer ID, status, and a cached response snapshot.
    • Constraints:
      • PRIMARY KEY on idempotency_key
      • CHECK (status IN ('IN_PROGRESS', 'COMPLETED'))
      • CHECK requiring a completed record to have both transfer_id and response_snapshot
    • Purpose:
      • Ensures duplicate requests with the same key are replayed safely and deterministically.
  • transfers

    • Represents the transfer record itself.
    • Constraints:
      • PRIMARY KEY on id
      • FOREIGN KEY to idempotency_records(idempotency_key)
      • FOREIGN KEY to source and destination wallets
      • CHECK (amount > 0)
      • CHECK (status IN ('PENDING', 'PROCESSED', 'FAILED'))
      • CHECK (from_wallet_id <> to_wallet_id)
    • Unique index:
      • ux_transfers_idempotency_key on idempotency_key
  • ledger_entries

    • Stores the double-entry ledger trail for every transfer.
    • Constraints:
      • PRIMARY KEY on id
      • FOREIGN KEY to transfers(id) and wallets
      • CHECK (entry_type IN ('DEBIT', 'CREDIT'))
      • CHECK (amount > 0)
      • UNIQUE (transfer_id, wallet_id, entry_type)
    • Indexes:
      • ix_ledger_wallet on (wallet_id, created_at, id)
      • ix_ledger_transfer on transfer_id

Idempotency Strategy

Idempotency is enforced using a unique idempotency_key plus a deterministic request fingerprint.

Request handling

  1. The request is normalized and a fingerprint is built from:

    • source wallet ID
    • destination wallet ID
    • amount
    • normalized currency
  2. The service tries to insert an idempotency_records row:

    • INSERT ... ON CONFLICT (idempotency_key) DO NOTHING
  3. If the insert succeeds:

    • this is the first request for that key
    • the transfer is created and linked to the idempotency record
  4. If the insert fails because the key already exists:

    • the existing record is loaded
    • if the fingerprint differs, the request is rejected as an idempotency conflict
    • if the record is COMPLETED, the service returns the cached response snapshot
    • if the record is still IN_PROGRESS, the request is treated as already being processed

This provides safe replay semantics: repeat requests with the same key return the same result instead of creating duplicate transfers.


Concurrency Strategy

The service prevents race conditions and double-spending by combining transactional row locks with atomic balance updates.

Protections in place

  • SELECT ... FOR UPDATE is used on the transfer row before processing

    • Prevents two concurrent workers from processing the same transfer at the same time
  • SELECT ... FOR UPDATE is used on both wallets involved in the transfer

    • Prevents concurrent modifications to the same wallet balances
  • Balance updates are applied in a single SQL statement using CASE

    • Debit and credit happen atomically within the same transaction
  • Ledger entries are inserted within the same transaction as the wallet updates

    • Ensures the financial trail stays consistent with the balance mutation
  • The schema uniqueness constraint on ledger_entries

    • prevents duplicate debit/credit entries for the same transfer wallet pair

This prevents:

  • wallet balance races
  • double-spending from concurrent debit attempts
  • inconsistent ledger and balance state under contention

How to Run

Local setup

  1. Start PostgreSQL:

    docker compose up -d postgres
  2. Apply migrations and seed data:

    make migrate-up
  3. Run the service:

    make run

Default configuration

  • API port: 8080
  • Database URL:
    postgres://wallet:wallet@localhost:5432/wallet_transfer?sslmode=disable
    

Useful commands

make test
make test-integration
make fmt
make fmt-check
make lint
make check-invariants
make migrate-down

Example endpoints

curl http://localhost:8080/healthz
curl -i -X POST http://localhost:8080/transfers \
  -H "Content-Type: application/json" \
  -d '{
    "idempotencyKey": "abc123",
    "fromWalletId": "00000000-0000-0000-0000-000000000001",
    "toWalletId": "00000000-0000-0000-0000-000000000002",
    "amount": 100
  }'

How to Test

Tradeoffs / Assumptions

  • Amounts are integer minor units. No floating point arithmetic is used.
  • Missing currency defaults to USD for assignment payload compatibility.
  • Service/domain code validates business rules and state transitions.
  • PostgreSQL transactions, row locks, and constraints enforce correctness under
    concurrent requests. It can reduce throughput under request load on the same wallets.

Checklist

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

@Prasen1
Prasen1 requested a review from amitlambakulu as a code owner July 19, 2026 16:11
Copilot AI review requested due to automatic review settings July 19, 2026 16:11
@Prasen1
Prasen1 marked this pull request as draft July 19, 2026 16:12

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 implements a Go-based wallet transfer service backed by PostgreSQL, focusing on idempotent POST /transfers, concurrency-safe balance updates, and double-entry ledger persistence, plus local tooling (migrations/Makefile/Docker) and test coverage.

Changes:

  • Added PostgreSQL schema/migrations for wallets, idempotency records, transfers, and ledger entries (plus seed + down migration).
  • Implemented HTTP API, service/domain layers, and Postgres repository/transaction layer for transfers and wallet reads.
  • Added unit + integration tests (including concurrent debit scenarios) and developer tooling/docs (Makefile, docker-compose, README, invariant checker).

Reviewed changes

Copilot reviewed 34 out of 36 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
README.md Updated run/test instructions and API examples for the Go service
migrations/001_init.sql Introduces core tables, constraints, and indexes
migrations/002_seed.sql Seeds two wallets for local/dev and integration tests
migrations/999_down.sql Adds down migration to drop tables
Makefile Adds common dev targets (run, migrate, lint, tests, invariant check)
internal/service/wallet_service.go Wallet read + ledger listing service logic
internal/service/transfer_service.go Idempotent transfer creation + processing workflow
internal/service/transfer_service_test.go Service unit tests with an in-memory fake store
internal/service/transfer_integration_test.go Postgres-backed integration tests incl. concurrency
internal/repository/postgres/tx.go Transaction-scoped Postgres operations (locks, writes)
internal/repository/postgres/store.go Postgres store with transaction helper + read methods
internal/repository/postgres/scan.go Row scanning helpers + domain error mapping
internal/repository/interfaces.go Store/Tx interfaces used by services
internal/infra/config.go Environment-based config loading
internal/domain/wallet.go Wallet domain model + debit/credit eligibility
internal/domain/wallet_test.go Unit tests for wallet domain rules
internal/domain/transfer.go Transfer model + state transitions
internal/domain/transfer_test.go Unit tests for transfer transitions/validation
internal/domain/money.go Money validation + currency normalization
internal/domain/money_test.go Unit tests for money validation/defaults
internal/domain/ledger_entry.go Ledger entry constructors for debit/credit legs
internal/domain/idempotency.go Idempotency record model
internal/domain/errors.go Shared domain errors
internal/api/wallets_handler.go Wallet HTTP handlers (wallet + ledger endpoints)
internal/api/transfers_handler.go Transfer HTTP handlers (create + get)
internal/api/server.go HTTP routing + request logging middleware
internal/api/server_test.go Handler-level tests for status codes and error mapping
internal/api/respond.go JSON/error response helpers and domain error mapping
IMPLEMENTATION_PLAN.md Documents intended architecture and milestones for the solution
go.mod Declares module and dependencies
go.sum Adds dependency checksums
docker-compose.yml Local Postgres container for development/testing
cmd/server/main.go Service entrypoint wiring API + services + Postgres
cmd/check-invariants/main.go CLI to validate ledger/balance invariants in DB
.gitignore Ignores Go build/cache directories
.github/workflows/ci.yml Fixes golangci-lint install step sequencing

Comment thread Makefile Outdated
Comment thread README.md
Comment thread migrations/001_init.sql
Comment thread internal/service/transfer_service_test.go
@Prasen1
Prasen1 marked this pull request as ready for review July 20, 2026 08:22
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