A Spring Boot REST API that calculates monthly and overall reward points for customers based on their transaction history.
For each transaction:
- 0 points on the portion of the amount at or below $50
- 1 point per dollar on the portion between $50 and $100
- 2 points per dollar on the portion above $100
Example: a $120 purchase earns (100-50)×1 + (120-100)×2 = 90 points.
The prompt leaves a few things unstated. Rather than guess silently, here's what this implementation assumes and why:
- Spend at or below $50 earns 0 points. Not stated explicitly, but required by the worked example: a $120 purchase yields exactly 90 points, which only holds if the first $50 contributes nothing.
- Tier boundaries are resolved by formula, not by picking inclusive/exclusive
bounds. Points are computed as
max(min(amount,100)-50, 0)×1 + max(amount-100, 0)×2. This makes $50.00 and $100.00 exactly well-defined without an arbitrary inclusive/exclusive choice at the boundary. - Points are rounded per transaction, not per month or per customer. Each transaction's raw point value is rounded to the nearest whole number (HALF_UP) before being summed, matching how a real system would post points at the time of purchase.
- Non-positive amounts (refunds, $0 transactions) earn 0 points rather than subtracting points. The prompt doesn't mention refunds, so this implementation treats them as reward-neutral rather than inferring a clawback policy.
- A customer ID with no transactions in the dataset returns HTTP 404. There's no separate customer registry in this assessment — only transaction records — so an unknown customer ID and a known customer with zero transactions in the period are indistinguishable.
- Months are grouped by the transaction's calendar date, not a billing cycle, since no billing cycle is defined.
- Currency is treated as a single, unspecified currency — no multi-currency or FX handling.
- A transaction amount whose calculated points would be too large to fit in
a standard integer (well beyond any realistic retail purchase) fails
loudly with
ArithmeticExceptionrather than silently producing a wrong number — kept as a one-line guard rather than a dedicated error type, since that scenario can't realistically occur with retail-scale amounts. - An unmapped route (e.g.
/rewards/with a trailing slash) correctly returns404, not500. Spring throwsNoResourceFoundExceptionfor any request that matches no endpoint, and that's anExceptionlike any other — a purely generic catch-all handler would have swallowed it and misreported it as a server error, so it's handled explicitly ahead of the catch-all.
controller → service → repository
↓
domain (RewardCalculator)
domain.RewardCalculator— the entire points formula, as a pure static method with no Spring dependency. This is deliberate: it's the one piece of logic where correctness really matters, so it's isolated and unit-testable with nothing else running.service.RewardServiceImpl— groups transactions by customer and calendar month and sums points. Contains no point-calculation logic of its own; it only orchestrates.repository.TransactionRepository— an interface with an in-memory implementation for this assessment. Transactions are indexed bycustomerIdin a map at load time, so a lookup is O(1) average case instead of scanning every transaction in the dataset on every request. A real deployment would swap this implementation for a JPA/database-backed one behind the same interface with no changes to the service or controller layers.config.SampleDataLoader— loadssample-transactions.jsoninto the repository at startup viaCommandLineRunner, so the API has data immediately with no manual setup step.model.Transaction/dto.*— Java 17 records, used for immutability and to avoid getter/setter/equals/hashCode boilerplate.exception.GlobalExceptionHandler— centralizes error handling via@RestControllerAdviceso every failure returns a consistent JSON shape instead of a raw stack trace or an inconsistent ad hoc error body.- BigDecimal is used for every currency calculation to avoid the rounding
errors that come with
double/floatfor money.
Base path: /api/v1/rewards
| Method | Path | Description |
|---|---|---|
| GET | /customers |
Monthly breakdown + total for every customer |
| GET | /customers/{customerId} |
Monthly breakdown + total for one customer |
| GET | /customers/{customerId}/monthly |
Just the monthly breakdown for one customer |
GET /api/v1/rewards/customers/CUST001
{
"customerId": "CUST001",
"monthlyBreakdown": [
{ "month": "2026-01", "points": 115 },
{ "month": "2026-02", "points": 50 },
{ "month": "2026-03", "points": 251 }
],
"totalPoints": 416
}Unknown customer:
GET /api/v1/rewards/customers/DOES-NOT-EXIST
→ 404 Not Found
{
"timestamp": "...",
"status": 404,
"message": "No transaction history found for customer: DOES-NOT-EXIST"
}
Unmapped route (e.g. a stray trailing slash, or any URL nothing is mapped to):
GET /api/v1/rewards/
→ 404 Not Found
{
"timestamp": "...",
"status": 404,
"message": "No endpoint found for the requested path."
}
src/main/resources/sample-transactions.json contains 14 transactions across
3 customers (CUST001, CUST002, CUST003) and 3 months (Jan–Mar 2026),
including boundary values ($50.00, $100.00 exactly) and decimal-cent amounts
to exercise the rounding logic.
Expected totals, for reference when reviewing test output:
| Customer | Jan | Feb | Mar | Total |
|---|---|---|---|---|
| CUST001 | 115 | 50 | 251 | 416 |
| CUST002 | 150 | 31 | 500 | 681 |
| CUST003 | 0 | 10 | 121 | 131 |
Requires Java 17+ and Maven (or use the included mvnw wrapper if you add
one — this repo assumes a local Maven install).
# Run the app (starts on http://localhost:8080)
mvn spring-boot:run
# Run the full test suite
mvn test
# Build a runnable jar
mvn clean package
java -jar target/rewards-api-1.0.0.jar- Java 17
- Spring Boot 3.3.4 (Web, Test)
- JUnit 5, Mockito, AssertJ
Out of scope for this assessment, but what I'd raise in a design review for a production version:
- Swap
InMemoryTransactionRepositoryfor a JPA-backed repository and real database - POST endpoint to ingest new transactions instead of a static bundled file
- Pagination and date-range filtering on the customer list/monthly endpoints
- OpenAPI/Swagger documentation
- AuthN/AuthZ on the endpoints