Skip to content

Feat/idr rate aggregator#191

Open
andhikakrstn3 wants to merge 12 commits intoallobankdev:mainfrom
andhikakrstn3:feat/idr-rate-aggregator
Open

Feat/idr rate aggregator#191
andhikakrstn3 wants to merge 12 commits intoallobankdev:mainfrom
andhikakrstn3:feat/idr-rate-aggregator

Conversation

@andhikakrstn3
Copy link
Copy Markdown

Summary

  • Implement single REST endpoint GET /api/finance/data/{resourceType} aggregating 3
    Frankfurter API resources
  • Apply Strategy Pattern with IDRDataFetcher interface and 3 concrete implementations
  • Use FactoryBean<WebClient> for API client construction
  • Fetch all data once at startup via ApplicationRunner, store in thread-safe immutable
    DataStoreService
  • Calculate personalized USD_BuySpread_IDR (spread factor: 0.00333)

Architectural Rationale

1. Polymorphism Justification (Strategy Pattern)

The Strategy Pattern was chosen over a simpler if/else or switch block because each of the
three data resources requires fundamentally different API endpoints, response parsing, and
transformation logic. By encapsulating each variation behind a common IDRDataFetcher
interface, we achieve:

  • Extensibility (Open/Closed Principle): Adding a new data resource requires only creating
    a new IDRDataFetcher implementation annotated with @Component. No existing code in the
    controller, runner, or other strategies needs to change.
  • Maintainability: Each strategy class is self-contained and independently testable. A bug
    fix or logic change in one strategy has zero risk of impacting others.
  • Elimination of conditional logic: The controller delegates using a simple map lookup by
    resourceType string, with no manual branching.

2. Client Factory (FactoryBean)

FrankfurterWebClientFactoryBean implements FactoryBean<WebClient> to encapsulate the full
construction and configuration of the WebClient instance within Spring's bean lifecycle.
Benefits over a standard @Bean method:

  • Encapsulation of complex creation logic: The FactoryBean pattern clearly separates the
    "how to build the client" concern into its own dedicated class.
  • Transparent bean registration: Spring treats the product of getObject() as the actual
    bean. Consumers inject WebClient directly without knowing a factory is involved.
  • Singleton management: The isSingleton() method ensures Spring caches and reuses the
    same WebClient instance across all strategy classes.

3. Startup Runner Choice (ApplicationRunner)

DataInitializationRunner implements ApplicationRunner to fetch and cache all exchange rate
data at startup. Advantages over @PostConstruct:

  • Full Spring context guarantee: ApplicationRunner executes after the entire application
    context is fully initialized. @PostConstruct runs during bean initialization, where dependent
    beans may not yet be ready.
  • Fail-fast behavior: If the Frankfurter API is unreachable, the application fails
    immediately at startup with a clear error.
  • Immediate responsiveness: Data is pre-loaded into an immutable in-memory store, so every
    API request is served instantly from memory.
  • Access to ApplicationArguments: Enables future flexibility such as passing custom
    parameters via command-line.

Personalization

  • GitHub Username: andhikakrstn3
  • ASCII Sum: 1333
  • Spread Factor: (1333 % 1000) / 100000.0 = 0.00333

Test Plan

  • Unit tests for all 3 strategy implementations (6 tests)
  • Integration test for startup data loading and endpoints (5 tests)
  • mvn verify passes (11 tests total)

Set up pom.xml with Spring Boot 3.3.6, Java 17, WebFlux (WebClient),
and test dependencies (MockWebServer). Add main application class
and application.yml with Frankfurter API base URL.
Add ApiResponse wrapper, LatestRateItem (with USD_BuySpread_IDR field),
HistoricalRateItem, and CurrencyItem as Java records.
Implement FactoryBean<WebClient> to construct and configure WebClient
with externalized base URL, response timeout, and JSON accept header.
Define strategy interface with getResourceType() and fetchAndTransform().
Implement spread factor calculation: (ASCII_SUM % 1000) / 100000.0
yielding 0.00333 for GitHub username "andhikakrstn3".
- LatestIDRRatesFetcher: fetches /latest?base=IDR, calculates USD_BuySpread_IDR
- HistoricalIDRUSDFetcher: fetches /2024-01-01..2024-01-05?from=IDR&to=USD
- SupportedCurrenciesFetcher: fetches /currencies
Use volatile Map + AtomicBoolean to ensure one-time initialization
and thread-safe read access. Data is stored as unmodifiable map.
Implement ApplicationRunner that iterates all IDRDataFetcher strategies,
collects data, and initializes DataStoreService once at startup.
Expose GET /api/finance/data/{resourceType} endpoint that reads from
DataStoreService without if/else branching. Handle 404, 503, and 500
errors via @ControllerAdvice.
Test each IDRDataFetcher with MockWebServer:
- LatestIDRRatesFetcherTest: verify USD_BuySpread_IDR calculation
- HistoricalIDRUSDFetcherTest: verify date sorting and rate parsing
- SupportedCurrenciesFetcherTest: verify code/name parsing and sort order
Verify DataStoreService is initialized after ApplicationRunner,
all three resource type endpoints return 200, and invalid resource
returns 404. Uses MockWebServer to stub Frankfurter API.
Add build/run instructions, cURL examples, personalization note
(spread factor 0.00333), and answers to the three architectural
rationale questions (Strategy Pattern, FactoryBean, ApplicationRunner).
…up in controller

- Add @JsonProperty("USD_BuySpread_IDR") to match spec field naming
- Inject Map<String, IDRDataFetcher> via Spring List in controller
  for map-based strategy lookup without if/else, as required by spec
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants