1. Overview
In the previous article, I covered how I solved the delay in main screen responses encountered while operating a MyData service by using a cache-first rendering and background refresh strategy—in other words, an approach aligned with the current Stale-While-Revalidate (SWR) pattern. This article covers the next challenge I encountered while operating the same service. This time, the problem was not performance but cost.
As the billing structure for external institution lookup APIs changed, the number of calls itself became an operating cost, making it difficult to continue using the existing approach of “looking up everything whenever necessary.” To address this, I applied timestamp-based conditional calls using lookup timestamps and redistributed when calls were made (batch refresh + Lazy Loading). Looking back, this approach—which I did not know by name at the time—was based on the same idea as conditional requests, a mechanism that the HTTP protocol has provided as a standard for a long time. This article presents the problem, the resolution process, and a reinterpretation from the perspective of standard mechanisms, in that order.
2. Problem: The Number of Calls Becomes a Cost
A MyData service is structured to collect users’ asset and transaction information by calling standard APIs from multiple external financial institutions. Even within a single refresh flow, sub-APIs are called successively, including institution-list lookups, account-list lookups, account-specific detail lookups, and transaction-history lookups. As a result, a considerable number of calls are generated even when refreshing the information of a single user once.
In the early stages of the service, this call volume was not a significant burden, but the situation changed when the billing structure shifted to one based on the number of calls. With the existing approach of looking everything up again at every refresh, regardless of whether anything had changed, the same cost was incurred even for calls that retrieved data that had not actually changed. As the number of users increased, this waste grew along with it.
While the response delay discussed in the previous article was a problem directly perceived by users, this challenge was invisible to users but directly related to the sustainability of operations. The problem can be restated as follows: “Do we really need to retrieve everything again every time just to check data that has not changed anyway?” Ultimately, the solution was not to eliminate calls altogether, but to identify and skip unnecessary calls.
3. Solution Strategy 1: Timestamp-Based Conditional Calls
The MyData standard API specification includes the concept of a Timestamp for handling the reference point in time for lookups. By using it, we can first check whether anything has changed since the previous lookup and, if nothing has changed, use the previously stored data as is while skipping calls to the lower-level APIs. Detailed lookups are performed only when changes are detected.
The processing flow can be summarized in pseudocode as follows.
function 자산정보갱신(사용자ID, 기관ID):
이전조회시점 = 조회이력.최근조회시점(사용자ID, 기관ID)
변경여부 = 외부기관API.변경확인(사용자ID, 이전조회시점)
if 변경여부 == 없음:
return 저장소.기존데이터(사용자ID, 기관ID)
최신데이터 = 외부기관API.상세조회(사용자ID) // 과금 대상 호출
저장소.갱신(사용자ID, 기관ID, 최신데이터)
조회이력.기록(사용자ID, 기관ID, 현재시각)
return 최신데이터
The key to this structure is leveraging the asymmetry that “the cost of asking is low, while the cost of retrieving is high.” We first filter using a lightweight call to check whether anything has changed, and limit the heavier call that retrieves the complete data to cases where a change has been confirmed. This structure also produces greater savings for users whose data does not change frequently.
4. Solution Strategy 2: Redistributing When Calls Are Made
If conditional calls were a mechanism for filtering “whether to make a call,” the second strategy was to redesign “when to make a call.” We combined two approaches.
The first is periodic batch refreshes. Based on the refresh interval selected by the user, data is refreshed at predetermined times in advance, regardless of whether the user enters the screen. Because refresh times become predictable, the volume of calls does not become concentrated at specific times, and already refreshed data can be displayed immediately when the user enters the screen.
The second is Lazy Loading for detailed data. Detailed data, such as transaction histories for individual accounts, is meaningful only when the user actually opens it. Therefore, instead of retrieving it in advance as part of the main refresh flow, we separated it so that it is requested when the user enters the relevant detail screen. Since not every user checks the details of every account, we were able to structurally eliminate calls for detailed data that was never actually viewed.
In short, the approach is to make calls “at the necessary time, and only to the necessary extent.” Frequently viewed summary information was prepared in advance through periodic batches, occasionally viewed detailed information was retrieved on demand through Lazy Loading, and a conditional check was placed before every call. This formed a three-part structure.
4-1. Structural Comparison
[Table 1] Comparison of the Call Structure Before and After Improvement
|
Category |
Existing Structure (As-Is) |
Improved Structure (To-Be) |
|---|---|---|
|
Call Decision |
Retrieve everything in a batch at every refresh |
Retrieve only what is necessary after checking for changes |
|
Call Timing |
Immediately when entering the screen or requesting a refresh |
Periodic batches + Lazy Loading when entering detail screens |
|
Call Volume Characteristics |
Increases in proportion to user activity |
Converges toward the frequency of data changes |
|
Detailed Data |
Retrieved in advance as part of the main flow |
Retrieved only on screens that the user actually enters |
5. In Retrospect: The Connection to HTTP Conditional Requests
5-1. A Mechanism HTTP Already Had
HTTP has a standard mechanism called a conditional request. When a client includes the modification time (Last-Modified) or version identifier (ETag) received with a previous response in the If-Modified-Since or If-None-Match header of the next request, the server returns only a 304 Not Modified response without a body if nothing has changed since then. The idea is to “ask first whether anything has changed, and omit transmitting the body if it has not.”
The Timestamp-based conditional calls in this case are based on the same idea as this mechanism. Checking for changes first based on the previous lookup time serves a role similar to If-Modified-Since, while skipping lower-level API calls and reusing stored data when nothing has changed has an effect similar to a 304 response. However, as with the distinction made in the previous article, the layers are different because this was not handled automatically at the protocol level through HTTP headers; instead, it was implemented directly in application code using the Timestamp specification of the standard API. This was not a matter of borrowing a mechanism provided by the protocol, but an example of independently implementing the same idea at the application level.
6. Relationship to the Previous Article: Two Axes of the Same Caching Model
What is interesting is that the SWR strategy from the previous article and the conditional calls in this article are two axes that coexist within the HTTP caching model. Their directions are opposite. SWR prioritizes response speed by following the strategy of “showing stale data first and performing validation later in the background.” Conditional requests prioritize transmission and call-cost savings by “validating first and not transmitting anything at all if nothing has changed.”
[Table 2] Comparison of the Two Caching Strategies
|
Item |
Stale-While-Revalidate (Previous Article) |
Conditional Requests (This Article) |
|---|---|---|
|
Basic Operation |
Immediately respond with stale data, then validate in the background |
Check for changes and receive data only when a change is detected |
|
Priority |
Response speed and perceived performance |
Reducing call and transmission costs |
|
What Is Saved |
User wait time |
Unnecessary calls and data transfer |
|
Points of application |
Response when entering the main screen |
Calls to query external institutions |
In the actual service, the two strategies coexisted at different points. We applied an SWR-oriented strategy to the screen responses users encountered, and conditional calls to outbound requests. Under a single caching theory, strategies moving in opposite directions were used in their respective places according to “what to save.”
7. Limitations and Trade-offs
The premise of this structure is that the Timestamp used as the basis for determining whether changes have occurred is reliable. If an external institution does not provide accurate change-time information, changes may be missed, or unnecessary re-fetches may occur instead. In actual operations, the level of specification compliance varied across external institutions, and the resulting data-quality issues required separate mitigation in their own right.
The acceptable range of refresh delays was another point that required continuous coordination. As conditional skips and periodic batches increase, costs decrease, but data freshness is pushed back accordingly. As with the cache retention-time criteria from the previous article, determining how much delay to allow for each type of data was an area requiring individual judgment, with no one-size-fits-all answer. Batch frequency also involved a balancing act: if it was too frequent, the savings were diluted, while if it was too infrequent, freshness declined.
8. Establishing Operational Guidelines
In the same way that we organized the screen-side cache retention times into a criteria table by data type in the previous article, this time we organized criteria for external-call strategies by data type. Rather than reconsidering the call method from scratch whenever a new data item was added, we made it possible to decide by matching the nature of the data to the criteria.
[Table 3] Criteria for Call Strategies by Data Type (Example)
|
Data type |
When to call |
Conditional check |
|---|---|---|
|
Asset summary (shown on the main screen) |
Periodic batch refresh |
Applied (skip if unchanged) |
|
Account details · Transaction history |
Deferred call upon entering the detail screen |
Applied (skip if unchanged) |
|
Data from a newly integrated institution |
Immediate full fetch right after integration |
Not applied (no history for comparison) |
This criteria table complements the cache-operation criteria table from the previous article. With the screen side establishing the criterion of “how old the data we show may be” and the call side establishing the criterion of “when and under what conditions to call,” the team became able to make decisions using the same language across the entire process, from data collection to display.
9. Conclusion
The response latency discussed in the previous article and the call costs discussed in this one appeared to be different problems, but after solving them, we found that they ultimately converged on the same question: “Is this call really necessary at this moment?” On the screen side, the answer was cache-first rendering; on the external-call side, it was conditional calls and rescheduling when calls were made.
And this time as well, we started with the problem and arrived at the same structure without knowing the name of the standard mechanism called a conditional request at the time. Through these two experiences, we were able to confirm more clearly that standards and patterns are ultimately answers organized by people who encountered the same problems first.
cryss