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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
target/
.idea/
*.iml
.classpath
.project
.settings/
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"explorer.compactFolders": false,
"java.configuration.updateBuildConfiguration": "automatic"
}
186 changes: 88 additions & 98 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,139 +1,129 @@
# Allo Bank Backend Developer Take-Home Test

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.
Spring Boot REST API for preloading and serving IDR-related exchange data from Frankfurter API using Strategy Pattern, custom `FactoryBean`, and startup data runner with immutable in-memory storage.

## 📝 Objective
## Tech Stack

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.
- Java 17
- Spring Boot 3.3.x
- Maven
- JUnit 5 + Mockito

The focus of this test is not just functional correctness, but demonstrating clean code, advanced Spring concepts, thread-safe design, and architectural clarity.
## Setup and Run

## I. Core Task: The Polymorphic API
1. Clone repository:

### 1. External API Integration (Frankfurter API)
```bash
git clone <your-repo-url>
cd allobank-backend-test
```

* **Base URL (Public):** `https://api.frankfurter.app/`.
2. Build application:

* You must integrate with three distinct data resources to enforce the architectural pattern:
```bash
mvn clean package
```

1. `/latest?base=IDR` (The latest rates relative to IDR)
3. Run application:

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.*
```bash
mvn spring-boot:run
```

3. `/currencies` (The list of all supported currency symbols)
4. Run tests:

### 2. Internal API Endpoint
```bash
mvn test
```

You must expose **one single endpoint** in your application: ```GET /api/finance/data/{resourceType}```
Application default port: `8080`

Where `{resourceType}` can be one of the three strings: `latest_idr_rates`, `historical_idr_usd`, or `supported_currencies`.
## API Endpoint

### 3. Required Functionality & Business Logic
Single endpoint:

* **Resource Handling:** Your service must correctly map the three incoming `resourceType` values to the correct data fetching strategies.
`GET /api/finance/data/{resourceType}`

* **Data Load:** All three resources should be fetched from the external API.
Supported `resourceType` values:

* **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.
- `latest_idr_rates`
- `historical_idr_usd`
- `supported_currencies`

**The Spread Factor Must Be Unique :**
Example cURL:

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.)*
```bash
curl http://localhost:8080/api/finance/data/latest_idr_rates
curl http://localhost:8080/api/finance/data/historical_idr_usd
curl http://localhost:8080/api/finance/data/supported_currencies
```

**Final Formula:** `USD_BuySpread_IDR = (1 / Rate_USD) * (1 + Spread Factor)` (where `Rate_USD` is the value from the API when `base=IDR`).
If unsupported `resourceType` is used, API returns `400 Bad Request`.

* **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.
## Personalization Note

## II. Architectural Constraints
- GitHub username used: `joniheri`
- ASCII sum (lowercase username): `856`
- Spread factor formula:
- `Spread Factor = (ASCII_SUM % 1000) / 100000.0`
- `Spread Factor = (856 % 1000) / 100000.0 = 0.00856`

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:
For `latest_idr_rates`, custom field is calculated as:

### Constraint A: The Strategy Pattern
`USD_BuySpread_IDR = (1 / Rate_USD) * (1 + Spread_Factor)`

The logic for handling the three different resources (`latest_idr_rates`, `historical_idr_usd`, `supported_currencies`) must be implemented using the **Strategy Design Pattern**.
## Architecture Summary

1. Define a clear **Strategy Interface** (e.g., `IDRDataFetcher`).
### Strategy Pattern

2. Implement **three concrete strategy classes** (one for each resource).
- Strategy interface: `IDRDataFetcher`
- Concrete strategies:
- `LatestIdrRatesFetcher`
- `HistoricalIdrUsdFetcher`
- `SupportedCurrenciesFetcher`
- Strategy lookup uses Spring-injected map through `IDRDataFetcherRegistry`.
- Controller and service do not use manual `if/else` or `switch` branching for resource dispatch.

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.
### Client Factory Bean

### Constraint B: Client Factory Bean
- External API client (`RestTemplate`) is created via custom `FactoryBean`:
- `RestTemplateFactoryBean`
- Base URL and client settings are externalized in `application.yml` via `FrankfurterApiProperties`.

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**.
### Startup Runner and Immutability

* 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).
- Data preload happens once on startup using `ApplicationRunner`:
- `FinanceDataPreloadRunner`
- All three resources are fetched and saved into `FinanceDataStore`.
- `FinanceDataStore` uses `AtomicReference` and deep immutable copy to enforce thread safety and immutability after initialization.
- API serves only from in-memory store after startup preload.

* ***You may not define the client as a simple `@Bean` in a `@Configuration` class.***
## Architectural Rationale

### Constraint C: Startup Data Runner & Immutability
1. Polymorphism Justification (Strategy Pattern)
The endpoint must serve three resource types with different upstream resources and transformation rules. Using `IDRDataFetcher` plus three concrete strategies keeps each behavior isolated and small. The controller/service layer only delegates by key through `IDRDataFetcherRegistry`, so adding a new resource type later only needs a new strategy class without touching existing branching logic. This improves extensibility and reduces regression risk when requirements grow.

The aggregated data for **ALL three resources** must be fetched **exactly once on application startup** and loaded into an in-memory store.
2. Client Factory Justification (`FactoryBean`)
`RestTemplateFactoryBean` centralizes external client construction concerns: timeout setup and base URL wiring from `frankfurter.api.base-url`. The `RestTemplate` is configured with `DefaultUriBuilderFactory(baseUrl)`, so client code only uses relative paths (`/latest`, `/{range}`, `/currencies`). This keeps endpoint composition consistent and removes repeated base URL concatenation in business/client code.

1. Use a Spring Boot **`ApplicationRunner`** or **`CommandLineRunner`** component to initiate the data fetching process.
3. Startup Runner Choice (`ApplicationRunner`)
`ApplicationRunner` is used so initial preload runs after Spring context and dependencies are fully ready, with clear startup lifecycle semantics and error handling options (`fail-fast`). Compared to `@PostConstruct`, runner-based initialization is easier to test, easier to control with properties, and cleaner for production startup orchestration.

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.
## Error Handling

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.
- Unsupported resource type -> `400`
- Data not initialized -> `503`
- Invalid numeric upstream values -> `400`
- Optional fallback payload on preload failure when `finance.preload.fail-fast=false`

## III. Production Readiness & Deliverables
## Test Coverage

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!
- Unit tests for each strategy:
- `LatestIdrRatesFetcherTest`
- `HistoricalIdrUsdFetcherTest`
- `SupportedCurrenciesFetcherTest`
- Service and store tests:
- `FinanceDataServiceTest`
- `FinanceDataStoreTest`
- Integration test for startup preloading:
- `FinanceDataPreloadRunnerIntegrationTest`
87 changes: 87 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>

<groupId>com.allobank</groupId>
<artifactId>allo-backend-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>allo-backend-test</name>
<description>Allo Bank Backend Developer Take-Home Test</description>

<properties>
<java.version>17</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.allobank.finance.AlloBankTestApplication</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Loading