Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/
3 changes: 3 additions & 0 deletions .mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
264 changes: 160 additions & 104 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,139 +1,195 @@
# Allo Bank Backend Developer Take-Home Test
# IDR Rate Aggregator API

Thank you for applying to our team! This take-home test is designed to evaluate your practical skills in building **production-ready** Spring Boot applications within a finance domain, focusing on architectural patterns and complex data handling.
A Spring Boot REST API that aggregates Indonesian Rupiah (IDR) exchange-rate data from the public [Frankfurter API](https://api.frankfurter.app) and exposes it through a single polymorphic endpoint.

---

## Setup & Run Instructions

### Prerequisites

| Tool | Minimum Version |
|---|---|
| Java JDK | 17 |
| Maven | 3.9+ |
| VS Code | Latest (with Extension Pack for Java) |

### Clone & Build

```bash
git clone https://github.com/MRafi68/allo-backend-test.git
cd allo-backend-test
mvn clean install
```

### Run the Application

```bash
mvn spring-boot:run
```

Or press **F5** in VS Code with the Extension Pack for Java installed.

The server starts on **port 8081** by default.

### Run Tests

```bash
# All tests
mvn test

# Unit tests only
mvn test -Dtest="*FetcherTest"

# Integration test only
mvn test -Dtest="DataLoaderRunnerIntegrationTest"
```

---

## Endpoint Usage

### Base URL
```
http://localhost:8081
```

### `GET /api/finance/data/{resourceType}`

| `resourceType` | Description |
|---|---|
| `latest_idr_rates` | Latest IDR exchange rates + USD_BuySpread_IDR |
| `historical_idr_usd` | IDR → USD time-series (2024-01-01 to 2024-01-05) |
| `supported_currencies` | Full list of supported currency codes and names |

### Example cURL Commands

## 📝 Objective
**1. Latest IDR Rates**
```bash
curl -X GET http://localhost:8081/api/finance/data/latest_idr_rates \
-H "Accept: application/json" | python -m json.tool
```

Your task is to create a single Spring Boot REST API endpoint capable of aggregating data from multiple, distinct resources provided by the public, keyless **Frankfurter Exchange Rate API**. The primary focus is on handling Indonesian Rupiah (IDR) data.
Expected response shape:
```json
{
"resourceType": "latest_idr_rates",
"data": [
{
"base": "IDR",
"date": "2025-02-26",
"rates": { "AUD": 0.000099, "USD": 0.000062, "..." : "..." },
"USD_BuySpread_IDR": 16137.84210000,
"spreadFactor": 0.00637
}
]
}
```

The focus of this test is not just functional correctness, but demonstrating clean code, advanced Spring concepts, thread-safe design, and architectural clarity.
**2. Historical IDR → USD**
```bash
curl -X GET http://localhost:8081/api/finance/data/historical_idr_usd \
-H "Accept: application/json" | python -m json.tool
```

## I. Core Task: The Polymorphic API
Expected response shape:
```json
{
"resourceType": "historical_idr_usd",
"data": [
{ "date": "2024-01-02", "rates": { "USD": 0.000064 } },
{ "date": "2024-01-03", "rates": { "USD": 0.000064 } },
{ "date": "2024-01-04", "rates": { "USD": 0.000063 } },
{ "date": "2024-01-05", "rates": { "USD": 0.000063 } }
]
}
```

### 1. External API Integration (Frankfurter API)
**3. Supported Currencies**
```bash
curl -X GET http://localhost:8081/api/finance/data/supported_currencies \
-H "Accept: application/json" | python -m json.tool
```

* **Base URL (Public):** `https://api.frankfurter.app/`.
Expected response shape:
```json
{
"resourceType": "supported_currencies",
"data": [
{ "code": "AUD", "name": "Australian Dollar" },
{ "code": "IDR", "name": "Indonesian Rupiah" },
{ "code": "USD", "name": "US Dollar" }
]
}
```

* You must integrate with three distinct data resources to enforce the architectural pattern:
**Invalid resource type (400 Bad Request)**
```bash
curl -X GET http://localhost:8081/api/finance/data/invalid_type
# Returns: 400 Bad Request
```

1. `/latest?base=IDR` (The latest rates relative to IDR)
---

2. **Historical Data:** Query a specific, small time series (e.g., `/2024-01-01..2024-01-05?from=IDR&to=USD`). **Note:** *Use the date range provided in this example unless a different range is communicated separately.*
## Personalization Note

3. `/currencies` (The list of all supported currency symbols)
**GitHub Username:** `MRafi68`

### 2. Internal API Endpoint
**Spread Factor Calculation:**

You must expose **one single endpoint** in your application: ```GET /api/finance/data/{resourceType}```
| Step | Value |
|---|---|
| Lowercase username | `mrafi68` |
| Unicode (ASCII) sum | `m(109) + r(114) + a(97) + f(102) + i(105) + 6(54) + 8(56) = 637` |
| Formula | `(637 % 1000) / 100000.0` |
| **Spread Factor** | **`0.00637`** |

Where `{resourceType}` can be one of the three strings: `latest_idr_rates`, `historical_idr_usd`, or `supported_currencies`.
**Final Formula:**
```
USD_BuySpread_IDR = (1 / Rate_USD) * (1 + 0.00637)
```

### 3. Required Functionality & Business Logic
Example: if `Rate_USD = 0.000062` then:
```
USD_BuySpread_IDR = (1 / 0.000062) * 1.00637 = 16231.00
```

* **Resource Handling:** Your service must correctly map the three incoming `resourceType` values to the correct data fetching strategies.
---

* **Data Load:** All three resources should be fetched from the external API.
## Architectural Rationale

* **Data Transformation (Latest IDR Rates only) - Unique Calculation:** For the **`latest_idr_rates`** resource, you must calculate and include a new field, `"USD_BuySpread_IDR"`. This is the Rupiah selling rate to USD after applying a banking spread/margin.
### i. Polymorphism Justification — Why Strategy Pattern?

**The Spread Factor Must Be Unique :**
The three resource types (`latest_idr_rates`, `historical_idr_usd`, `supported_currencies`) each require distinct fetch logic, URL construction, and data transformation. A naive implementation would use a cascading `if/else` or `switch` block in the service or controller layer.

1. **Input:** Your GitHub username (e.g., `johndoe47`).
2. **Calculation:** Calculate the sum of the Unicode (ASCII) values of all characters in your lowercase GitHub username string.
3. **Spread Factor Derivation:** `Spread Factor = (Sum of Unicode Values % 1000) / 100000.0`
*(This will yield a unique factor between 0.00000 and 0.00999, ensuring a personalized result.)*
**The Strategy Pattern was chosen instead for two key reasons:**

**Final Formula:** `USD_BuySpread_IDR = (1 / Rate_USD) * (1 + Spread Factor)` (where `Rate_USD` is the value from the API when `base=IDR`).
**Extensibility (Open/Closed Principle):** Adding a fourth resource type requires only creating a new class that implements `IDRDataFetcher` — no existing code is modified. The controller automatically discovers it via Spring's dependency injection of all `IDRDataFetcher` beans into a `Map<String, IDRDataFetcher>`. This means the routing logic scales to N resources with zero code changes to the controller or service.

* **Other Resources:** The `historical_idr_usd` and `supported_currencies` resources can return their data with minimal transformation, but the final output must be a unified JSON array of results.
**Maintainability:** Each strategy is a self-contained, independently testable unit. A bug in the historical rates transformation cannot affect the currency list logic. Each class has a single reason to change. The `getResourceType()` method serves as a self-registering key, eliminating the need for any external mapping configuration.

## II. Architectural Constraints
---

Meeting the core task is only one part of the solution. The following constraints must be strictly adhered to and will be heavily weighted during evaluation:
### ii. Client Factory Bean — Why `FactoryBean<WebClient>` over `@Bean`?

### Constraint A: The Strategy Pattern
Spring's `FactoryBean<T>` interface is a dedicated creational contract for complex object construction. It was chosen over a simple `@Bean` method in a `@Configuration` class for the following reasons:

The logic for handling the three different resources (`latest_idr_rates`, `historical_idr_usd`, `supported_currencies`) must be implemented using the **Strategy Design Pattern**.
A `@Bean` method in `@Configuration` is a general-purpose object provider with no lifecycle semantics beyond creation. A `FactoryBean` makes the construction intent explicit — the class *is* a factory, not a configuration class that happens to produce a bean among many others. This enforces **Single Responsibility**: `WebClientFactoryBean` has exactly one job.

1. Define a clear **Strategy Interface** (e.g., `IDRDataFetcher`).
`FactoryBean` also provides `getObjectType()` and `isSingleton()` as first-class methods, giving Spring richer metadata about the produced object for proxy generation and context management. Additionally, it encapsulates all WebClient configuration (base URL, headers, codec setup, timeouts) in one place, keeping `@Configuration` classes clean and free of infrastructure concerns.

2. Implement **three concrete strategy classes** (one for each resource).
---

3. The main `Controller` should dynamically select the correct strategy implementation using a map-based lookup injected by Spring, avoiding any manual `if/else` or `switch` logic in the controller layer.
### iii. Startup Runner Choice — Why `ApplicationRunner` over `@PostConstruct`?

### Constraint B: Client Factory Bean
Both `ApplicationRunner` and `@PostConstruct` run code after bean creation, but they differ in timing and context:

The instance of your chosen external API client (`WebClient` or `RestTemplate`) **must be defined and created within a custom implementation of Spring's `FactoryBean<T>` interface**.
`@PostConstruct` runs during bean initialisation, **before** the full application context is ready. This means other beans that the data-loading logic depends on (such as the `WebClient` or all `IDRDataFetcher` strategies) may not yet be fully initialised, creating subtle ordering bugs.

* This `FactoryBean` should be responsible for externalizing the API Base URL via `@Value` or `@ConfigurationProperties` and applying any initial configuration (e.g., timeouts, shared headers).
`ApplicationRunner` runs **after the entire Spring context is fully refreshed and ready**, guaranteeing that every bean, configuration, and dependency is available. This is the correct hook for application-level startup tasks like pre-loading data.

* ***You may not define the client as a simple `@Bean` in a `@Configuration` class.***
Furthermore, `ApplicationRunner` receives `ApplicationArguments`, making it easy to add command-line-driven behaviour in the future (e.g., `--refresh-data` flag). `@PostConstruct` has no such extension point.

### Constraint C: Startup Data Runner & Immutability
The `.block()` call in the runner is intentional and correct here: the application must not begin serving requests until the in-memory store is populated, and blocking at startup (not at request time) achieves this without any concurrency risk.

The aggregated data for **ALL three resources** must be fetched **exactly once on application startup** and loaded into an in-memory store.

1. Use a Spring Boot **`ApplicationRunner`** or **`CommandLineRunner`** component to initiate the data fetching process.

2. The API endpoint (`GET /api/finance/data/{resourceType}`) must serve the data from this **in-memory store**, not by making a new call to the external API on every request.

3. The in-memory storage mechanism (e.g., a service holding the data) must be designed to be **thread-safe** and ensure the data is **immutable** once the `ApplicationRunner` has finished loading it.

## III. Production Readiness & Deliverables

Your final solution must demonstrate production quality through code, testing, and communication.

### 1. Robustness & Best Practices

* Graceful **Error Handling** for network failures or 4xx/5xx responses from the external API.

* Proper use of **Configuration Properties** (e.g., `application.yml`) for external service URLs.

* Clear separation of concerns (Controller, Service, Model/DTO, etc.).

### 2. Testing

* **Unit Tests** for all three `IDRDataFetcher` strategy implementations, ensuring data calculation and transformation logic is covered (using mock clients for external calls).

* **Integration Tests** to verify the `ApplicationRunner` successfully initializes and loads the data into the in-memory store before the application context is ready.

### 3. Documentation

A clear `README.md` is mandatory. It must include:

* **Setup/Run Instructions:** Clear steps to clone, build, and run the application and tests.

* **Endpoint Usage:** Example cURL commands to test the three different resource types.

* **Personalization Note:** Clearly state your GitHub username and show the exact **Spread Factor** (e.g., `0.00765`) calculated by your function.

* ---

* ### 🛠️ Architectural Rationale

This section should contain a brief, but detailed, explanation answering the following questions:

1. **Polymorphism Justification:** Explain *why* the Strategy Pattern was used over a simpler conditional block in the service layer for handling the multi-resource endpoint. Discuss the benefits in terms of **extensibility** and **maintainability**.

2. **Client Factory:** Explain the specific role and benefit of using a **`FactoryBean`** to construct the external API client. Why is this preferable to defining the client using a standard `@Bean` method in this scenario?

3. **Startup Runner Choice:** Justify the choice of using an `ApplicationRunner` (or `CommandLineRunner`) for the initial data ingestion over a simpler `@PostConstruct` method.

## IV. Submission & Review Process

1. **Fork** this repository.

2. Implement your solution on a dedicated feature branch (e.g., `feat/idr-rate-aggregator`).

3. When complete, submit your solution via a **Pull Request (PR)** back to the main repository.
4. Please complete the form to submit your technical test: [Click Here](https://forms.gle/nZKQ2EjTCPfAKHog7)

**Your PR will be evaluated on the following:**

* **Commit History:** Clean, atomic, and descriptive commit messages (e.g., "feat: Implement IDR latest rates strategy," "fix: Correctly calculate IDR spread in tests").

* **PR Description:** The description must clearly summarize the solution and **must contain the full answers** to the three "Architectural Rationale" questions from Section III.

* **Code Review Readiness:** The code should be well-structured and ready for immediate review.

Good luck!
---
Loading