Skip to content

Tales-K/wallet-api

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Wallet Service (Java) - Mission-Critical, Auditable, Horizontally Scalable

This service manages user wallets with operations to create wallets, retrieve balances (current and historical), and post deposits, withdrawals, and transfers. It is designed to run multiple stateless instances concurrently without distributed locks, while preventing duplicates and preserving a complete audit trail.

Contents

  1. How to run the project
  2. Decisions
  3. Assumptions
  4. Architecture Overview
  5. Infrastructure
  6. Running tests
  7. Running the API locally
  8. Time tracking

How to run the project

Pre-requisites:

  • Docker and Docker Compose.
  • (Optional) Java 21 for unit/integration tests

1) Run all services (API, db, load-balancer, monitoring and tracing):

  1. Clone this repo and run all commands in its root.
  2. Make sure your docker engine is running.
docker compose \
  -f infra/docker-compose.yml \
  -f infra/docker-compose.lb.yml \
  -f infra/docker-compose.observability.yml \
  -f infra/docker-compose.zipkin.yml \
  up -d --build --scale app=3

2) Check API status

Credentials:

user: admin
password: admin

3) Run requests

Option 1

Run manually through the API documentation (has endpoints, schemas and errors). Here you can create wallets, do deposits, withdrawals and transfers, and check balances and ledgers.

Option 2

Run a load test and do lots of requests:

K6_SCRIPT=transfers.js docker compose -f infra/docker-compose.k6.yml run --rm k6

You can replace transfers.js with withdraws.js or deposits.js to test those operations.

Decisions

  • Given the importance of the project, I decided to skip authentication implementation in order to focus on consistency and performance.
  • I found that idempotency is crucial for this project, so I implemented it using a combination of a unique request identifier and database transaction.
  • I designed the system as stateless, which allows for easy scaling and load balancing.
  • Future cleanup schedules should be implemented to clean old idempotency keys.
  • There are two ways of fetching balance history: by informing desired date and time, and checking ledgers by time range filter.

Assumptions

  • Single currency; amounts stored as DECIMAL(19,4).
  • No overdrafts: balances must never go negative.
  • Only “now” postings (no backdated/future-dated effective_at).
  • All IDs server-generated UUID v4 (wallet_id, tx_id), except for idempotency_key.
  • Idempotency-Key header is required for all POSTs; responses are cached for safe retries.
  • Idempotency state machine is intentionally simple: only in_progress, succeeded and failed.
    • An in_progress entry is considered stale after 5 seconds.
    • If a retry arrives after that window, the stale in_progress is treated as abandoned and the retry proceeds (stale takeover). This ensures transient errors never block progress.
  • The ledger is the audit source of truth.

Architecture Overview

  • Each request that mutates state runs in a single ACID transaction:
    • Validate input and Idempotency-Key.
    • Check idempotency cache; if present and completed, return cached result.
    • Execute atomic SQL (conditional updates + inserts).
    • Insert append-only ledger entries.
    • Store final response in idempotency_keys.
  • Transfers update two wallets within one transaction.

Infrastructure

  • Docker Compose is used to orchestrate the services.
  • Added observability stack (Grafana, Loki, Promtail) for metrics and logs.
  • Added tracing stack (Micrometer, Zipkin) for distributed tracing.
  • Nginx is used as a load balancer to distribute requests across multiple API instances.
  • k6 is used for load testing the API.
  • Testcontainers are used for integration tests.

Running tests

Unit tests

Classic unit tests made with JUnit and Mockito.

./api/mvnw -f api/pom.xml test

Integration tests

Made with Testcontainers, they run a real Postgres instance in a container.

./api/mvnw -f api/pom.xml -Dit.test=ConcurrencyIT failsafe:integration-test failsafe:verify

Load tests

Made with k6, they target the load-balancer so it spreads across 3 instances.

K6_SCRIPT=transfers.js docker compose -f infra/docker-compose.k6.yml run --rm k6
K6_SCRIPT=deposits.js docker compose -f infra/docker-compose.k6.yml run --rm k6
K6_SCRIPT=withdraws.js docker compose -f infra/docker-compose.k6.yml run --rm k6

Turning all of

docker compose \
  -f infra/docker-compose.yml \
  -f infra/docker-compose.lb.yml \
  -f infra/docker-compose.observability.yml \
  -f infra/docker-compose.zipkin.yml \
  down

Running the API locally

Requires JDK 21, Maven and Postgres

  1. Run a Postgres in a container
docker compose -f infra/docker-compose.yml up db -d
  1. Run the API locally:
./api/mvnw -f api/pom.xml spring-boot:run
  1. Check documentation in local Swagger-UI

You may have to disable CORS validation in your browser.

Time tracking

    [1h] - research (atomicity and idempotency patterns)
    [2h] - development of transaction entities
    [2h] - implementation of idempotency
    [2h] - testing (unit, integration and load) and bugfix
    [2h] - infra setup
    [1h] - documentation and review

About

API for handling bank wallet transactions

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages