A tiny in-memory REST API for recording money movements.
A small ledger that supports deposits, withdrawals, balance queries and transaction history.
This document describes scopes, assumptions and trade-offs.
This application manages one ledger, which represents one account balance.
I intentionally did not introduce User, Account or Wallet entities because the assignment only asks for deposits, withdrawals, current balance and transaction history. Adding multiple accounts would require additional API design, account identity, ownership rules and transfer semantics.
Deposits and withdrawals are treated as external movements into or out of the ledger.
- A deposit increases the ledger balance.
- A withdrawal decreases the ledger balance.
Transfers between accounts are intentionally out of scope because they would require modelling at least two accounts and ensuring both debit and credit entries are created atomically.
Transaction amounts are always positive.
The transaction type defines the effect on the balance:
DEPOSITadds the amount.WITHDRAWALsubtracts the amount.
All amounts are assumed to be in one currency.
Multi-currency support is intentionally excluded because it introduces extra rules such as currency codes, decimal precision per currency, exchange rates and conversion logic.
Monetary values are represented with BigDecimal.
double and float are intentionally avoided because floating-point arithmetic can introduce rounding errors.
Amounts are validated to be:
- present
- greater than zero
- no more than two decimal places
Transactions are append-only.
The API does not support updating or deleting transactions because transaction history acts as the audit log for the ledger.
The application uses in-memory data structures, as suggested by the assignment.
Data is lost when the application restarts.
I considered H2, but decided against it because it would shift the exercise from a tiny ledger API into persistence design. Since the assignment explicitly suggested maps or arrays, I kept storage in memory and documented how I would handle persistence and database concurrency in production.
The service stores both current balance and transaction history. Both are updated together inside the same critical section (synchronized block) to keep them consistent.
The lock protects:
- Check the current balance
- Calculate the new balance
- Create the transaction record
- Update the balance
- Append the transaction to history
This prevents race conditions such as two concurrent withdrawals both seeing the same old balance.
This is a simple single-JVM solution. In production, consistency would be handled with database transactions or optimistic locking.
- Validation errors →
400 Bad Request - Withdrawal exceeds balance →
409 Conflict
| Feature | Reason |
|---|---|
| Users | Authentication and authorization are out of scope |
| Accounts | Assignment can be satisfied with a single ledger |
| Currency | Multi-currency introduces rules not required here |
| Database | Assignment suggests in-memory storage |
| Docker | App should run locally without extra software |
| Event Sourcing/CQRS | Tiny implementation, not a production platform |
| Monitoring/Logging | Not expected for this assignment |
- Java 26
./gradlew bootRunApp starts at http://localhost:8080
./gradlew testOnce running, visit:
http://localhost:8080/swagger-ui.html
| Method | Endpoint | Description |
|---|---|---|
POST |
/transactions |
Create a deposit or withdrawal |
GET |
/transactions |
Get transaction history |
GET |
/balance |
Get current balance |
curl -X POST http://localhost:8080/transactions \
-H "Content-Type: application/json" \
-d '{"type": "DEPOSIT", "amount": 100.00}'{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "DEPOSIT",
"amount": 100.00,
"balanceAfter": 100.00,
"createdAt": "2026-05-20T10:30:00Z"
}curl -X POST http://localhost:8080/transactions \
-H "Content-Type: application/json" \
-d '{"type": "WITHDRAWAL", "amount": 30.00}'{
"id": "550e8400-e29b-41d4-a716-446655440001",
"type": "WITHDRAWAL",
"amount": 30.00,
"balanceAfter": 70.00,
"createdAt": "2026-05-20T10:31:00Z"
}curl http://localhost:8080/balance{
"balance": 70.00
}curl http://localhost:8080/transactions[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "DEPOSIT",
"amount": 100.00,
"balanceAfter": 100.00,
"createdAt": "2026-05-20T10:30:00Z"
},
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"type": "WITHDRAWAL",
"amount": 30.00,
"balanceAfter": 70.00,
"createdAt": "2026-05-20T10:31:00Z"
}
]curl -X POST http://localhost:8080/transactions \
-H "Content-Type: application/json" \
-d '{"type": "WITHDRAWAL", "amount": 1000.00}'{
"timestamp": "2026-05-20T10:32:00Z",
"status": 409,
"error": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance: current 70.00, requested withdrawal 1000.00"
}curl -X POST http://localhost:8080/transactions \
-H "Content-Type: application/json" \
-d '{"type": "DEPOSIT", "amount": -50.00}'{
"timestamp": "2026-05-20T10:33:00Z",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"details": ["amount: Amount must be greater than zero"]
}If this were a production system:
- Persistent storage — PostgreSQL or similar
- Database transactions — atomicity of balance updates
- Row-level / optimistic locking — prevent race conditions
- Idempotency keys — prevent duplicate transactions from retries
- Multiple accounts — accounts with unique identifiers
- Multi-currency — currency codes, exchange rates
- Pagination — for transaction history
- Authentication - some modern framework like Oauth 2.0
- Audit/reversal flows — transaction reversals with audit trails
- Observability — metrics, tracing, health checks
- Structured logging — JSON logging for production debugging
- Java 26
- Spring Boot 4.0.6
- Gradle
- Spring Web MVC
- Bean Validation
- Lombok
- JUnit 5
- SpringDoc OpenAPI (Swagger)