Open
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Architectural Rationale
i. Polymorphism Justification — Why Strategy Pattern?
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 cascadingif/elseorswitchblock in the service or controller layer.The Strategy Pattern was chosen for two key reasons:
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 allIDRDataFetcherbeans into aMap<String, IDRDataFetcher>. This means the routing logic scales to N resources with zero code changes to the controller or service.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. Client Factory Bean — Why
FactoryBean<WebClient>over@Bean?Spring's
FactoryBean<T>interface is a dedicated creational contract for complex object construction. It was chosen over a simple@Beanmethod in a@Configurationclass for the following reasons:A
@Beanmethod in@Configurationis a general-purpose object provider with no lifecycle semantics beyond creation. AFactoryBeanmakes 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:WebClientFactoryBeanhas exactly one job.FactoryBeanalso providesgetObjectType()andisSingleton()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@Configurationclasses clean and free of infrastructure concerns.iii. Startup Runner Choice — Why
ApplicationRunnerover@PostConstruct?Both
ApplicationRunnerand@PostConstructrun code after bean creation, but they differ in timing and context:@PostConstructruns during bean initialisation, before the full application context is ready. This means other beans that the data-loading logic depends on (such as theWebClientor allIDRDataFetcherstrategies) may not yet be fully initialised, creating subtle ordering bugs.ApplicationRunnerruns 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.Furthermore,
ApplicationRunnerreceivesApplicationArguments, making it easy to add command-line-driven behaviour in the future (e.g.,--refresh-dataflag).@PostConstructhas no such extension point.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.