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
72 changes: 72 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# ========================
# BUILD FILES
# ========================
/target/
*.class
*.jar
*.war
*.ear

# ========================
# LOG FILES
# ========================
*.log

# ========================
# IDE - IntelliJ
# ========================
.idea/
*.iws
*.iml
*.ipr
out/

# ========================
# IDE - Eclipse
# ========================
.project
.classpath
.settings/

# ========================
# IDE - VS Code
# ========================
.vscode/

# ========================
# OS FILES
# ========================
.DS_Store
Thumbs.db

# ========================
# ENV FILES
# ========================
.env
.env.*

# ========================
# MAVEN
# ========================
.mvn/wrapper/maven-wrapper.jar
.mvn/wrapper/maven-wrapper.properties

# ========================
# SPRING BOOT
# ========================
/logs/
*.pid

# ========================
# TEST OUTPUT
# ========================
/surefire-reports/
/failsafe-reports/

# ========================
# TEMP FILES
# ========================
*.tmp
*.swp
*.lst
*.txt
171 changes: 91 additions & 80 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,139 +1,150 @@
# Allo Bank Backend Developer Take-Home Test
## Ringkasan

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.
PR ini berisi implementasi REST API untuk mengagregasi data nilai tukar IDR dari Frankfurter API sesuai dengan requirement.

## 📝 Objective
Data yang disediakan mencakup:

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.
* Kurs terbaru terhadap IDR
* Data historis IDR ke USD
* Daftar mata uang yang tersedia

The focus of this test is not just functional correctness, but demonstrating clean code, advanced Spring concepts, thread-safe design, and architectural clarity.
Seluruh data diambil satu kali saat aplikasi start dan disimpan di memory, sehingga endpoint tidak melakukan request ulang ke API eksternal.

## I. Core Task: The Polymorphic API
---

### 1. External API Integration (Frankfurter API)
## Fitur yang Diimplementasikan

* **Base URL (Public):** `https://api.frankfurter.app/`.
* Endpoint:

* You must integrate with three distinct data resources to enforce the architectural pattern:
* `GET /api/finance/data/{resourceType}`

1. `/latest?base=IDR` (The latest rates relative to IDR)
* Resource yang didukung:

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

3. `/currencies` (The list of all supported currency symbols)
* Penambahan field:

### 2. Internal API Endpoint
* `USD_BuySpread_IDR` (khusus latest rate)

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

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

### 3. Required Functionality & Business Logic
### Strategy Pattern

* **Resource Handling:** Your service must correctly map the three incoming `resourceType` values to the correct data fetching strategies.
Digunakan untuk memisahkan logic pengambilan data berdasarkan resource.

* **Data Load:** All three resources should be fetched from the external API.
Setiap resource memiliki implementasi sendiri dari interface `IDRDataFetcher`, sehingga tidak perlu menggunakan if/else di controller.

* **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.
Pendekatan ini memudahkan jika ingin menambahkan resource baru ke depannya.

**The Spread Factor Must Be Unique :**
---

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.)*
### FactoryBean untuk WebClient

**Final Formula:** `USD_BuySpread_IDR = (1 / Rate_USD) * (1 + Spread Factor)` (where `Rate_USD` is the value from the API when `base=IDR`).
WebClient dibuat menggunakan FactoryBean agar konfigurasi seperti base URL dan timeout terpusat.

* **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.
Dengan cara ini, pembuatan client tidak tersebar di berbagai class dan lebih mudah dikelola.

## 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:
### ApplicationRunner

### Constraint A: The Strategy Pattern
Digunakan untuk load data dari external API saat aplikasi start.

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

1. Define a clear **Strategy Interface** (e.g., `IDRDataFetcher`).
* memastikan data hanya diambil sekali
* menghindari pemanggilan API berulang
* memastikan semua dependency sudah siap sebelum proses fetch

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.
## Response Design

### Constraint B: Client Factory Bean
Response dibuat konsisten menggunakan wrapper:

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**.
```json
[
{
"resourceType": "latest_idr_rates",
"data": { ... }
}
]
```

* 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).
Pendekatan ini memudahkan parsing di sisi client dan menjaga konsistensi antar endpoint.

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

### Constraint C: Startup Data Runner & Immutability
## Error Handling & Timeout

The aggregated data for **ALL three resources** must be fetched **exactly once on application startup** and loaded into an in-memory store.
* Menambahkan timeout pada pemanggilan WebClient
* Menangani error dari external API secara graceful
* Menambahkan GlobalExceptionHandler untuk menangani error umum

1. Use a Spring Boot **`ApplicationRunner`** or **`CommandLineRunner`** component to initiate the data fetching process.
Contoh response saat error:

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.
```json
{
"error": "Failed to fetch data",
"message": "TimeoutException"
}
```

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
## Testing

Your final solution must demonstrate production quality through code, testing, and communication.
Unit test mencakup:

### 1. Robustness & Best Practices
* Strategy (latest, historical, currencies)
* SpreadUtil
* DataLoader

* Graceful **Error Handling** for network failures or 4xx/5xx responses from the external API.
WebClient dimock agar test tidak bergantung ke API eksternal.

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

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

### 2. Testing
```bash
mvn clean install
mvn spring-boot:run
```

* **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.
## Contoh Penggunaan

### 3. Documentation
```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
```

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

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

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

* **Personalization Note:** Clearly state your GitHub username and show the exact **Spread Factor** (e.g., `0.00765`) calculated by your function.
Spread dihitung menggunakan:
(sum ASCII username % 1000) / 100000.0

* ---
---

* ### 🛠️ Architectural Rationale
## Catatan

This section should contain a brief, but detailed, explanation answering the following questions:
* Data disimpan dalam bentuk immutable setelah load
* Menggunakan BigDecimal untuk menjaga presisi perhitungan
* Endpoint hanya membaca dari memory (tidak hit API lagi)

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?
## Penutup

3. **Startup Runner Choice:** Justify the choice of using an `ApplicationRunner` (or `CommandLineRunner`) for the initial data ingestion over a simpler `@PostConstruct` method.
Implementasi ini dibuat dengan fokus pada clean architecture, maintainability, dan testability.

## 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!
33 changes: 33 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.allo</groupId>
<artifactId>finance</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
12 changes: 12 additions & 0 deletions src/main/java/com/allo/finance/FinanceApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

package com.allo.finance;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FinanceApplication {
public static void main(String[] args) {
SpringApplication.run(FinanceApplication.class, args);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/allo/finance/config/JacksonConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.allo.finance.config;

import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;

@Configuration
public class JacksonConfig {

@SuppressWarnings("deprecation")
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> builder.featuresToEnable(
SerializationFeature.WRITE_BIGDECIMAL_AS_PLAIN
);
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/allo/finance/config/WebClientFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

package com.allo.finance.config;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

@Component
public class WebClientFactory implements FactoryBean<WebClient> {

@Value("${external.api.base-url}")
private String baseUrl;

@Override
public WebClient getObject() {
return WebClient.builder().baseUrl(baseUrl).build();
}

@Override
public Class<?> getObjectType() {
return WebClient.class;
}
}
Loading