DEV Community

Sheraz Manzoor
Sheraz Manzoor

Posted on AI-assisted

How a smart Engineering Decision Reduced Our costs by 25% and is continuously reducing.

Our banking app had a feature, which let the users to find nearby branches and ATMs using their current location. And the users liked this feature very much.
It was straightforward from the user's perspective.

open app -> open maps tab -> find the branches and ATMs
Enter fullscreen mode Exit fullscreen mode

But with a banking app, with a MILLIONS OF USERS, it was very costly. A user might request maps for several times a day and that will hit google maps api which bills based on the number of requests.

Consider a typical user journey. Someone opens the map before visiting a branch. They close it, return a few minutes later, and search for nearby ATMs. Another user makes a similar request in the same area. With a large user base, these otherwise ordinary interactions can generate a substantial number of external API requests.

Our orignal request flow was very simple.

flowchart LR
    A[User opens map] --> B[Application requests locations]
    B --> C[Google Maps API]
    C --> D[Results displayed]

There was nothing wrong with Google Maps. It provided the functionality we needed. The problem was how our application approached repeated requests.

That led us to a practical question:

Why should we make another external request for information we already have, when that information can legally and reliably be reused?

The objective wasn't to eliminate Google Maps. We still needed it when the application genuinely required information that wasn't available locally or needed a permitted refresh.

The objective was to stop treating every lookup as if it were the first one.

Let's store the map's data in our own database

This idea changed the whole picture. We decided when user opens the map, we get data from Google maps, and we store this data in our own database.
AND when the user opens the map again, we make the request to our database first, if requested data is there, we don't have to call the Google Maps API again. And if data is not there, the Google Maps will be requested to provide the required data and that will be stored in the database again.

It was attractive for a few reasons:

  • Fewer unnecessary external requests.
  • Lower recurring API spend.
  • Fewer network round trips on a path users hit constantly.
  • Reuse of information across requests, where reuse is appropriate and permitted.
  • Faster responses for frequent lookups.

A word on terminology, because it matters here. The persistent layer is information stored in our database. Redis, which came later, has a different and narrower role. And renaming "cache" to "database storage" changes nothing about what Google allows. The Terms prohibit caching Google Maps Content except where expressly permitted, and they list storing Google Maps Content outside the services among the things you can't do (Terms of Service, ยง3.2.3). Where the data sits doesn't decide whether you may keep it.

Implementation (2 layers, 2 jobs)

SInce we established database first strategy, we had to make lookups fast enough, that user don't notice the delay.

Earlier approach:

user opens map -> request sent to google maps server -> response back to client (user)
ONE API CALL ONLY
Enter fullscreen mode Exit fullscreen mode

But with our idea

user open map -> request sent to bank server -> result not found -> request sent again to google maps api -> response back to client (user)
TWO API CALLS IF DATA IS NOT AVAILABLE IN OUR SERVER
Enter fullscreen mode Exit fullscreen mode

So that was another challenge. So we found a way to manage this. RADIS !!

Initally Radis has all pointers about every data, that this specific requested data is not present in our server. So, the initial request is now never sent to our server if the requested data is not there.

And whenever that data or information is stored in database, the pointer to that specific data from the Radis Cache was removed.

Thus when user made the request, the first place to gp was Radis Cache. If data pointer is there, we would make the request directly to Google Maps API instead of hiting our server first. And if the data pointer was not in the Radis Cache, we made the requests to our server directly.
Speed lightening fast, least requests possible sent to the Google Maps API.

flowchart TD
A[User opens map] --> B{Check Redis Cache}

    B -->|Pointer exists: data unavailable locally| C[Request Google Maps API]
    C --> D[Return response to user]

    B -->|Pointer does not exist: data may be in database| E[Request Bank Server]
    E --> F{Data found in database?}

    F -->|Yes| G[Return stored data]
    G --> D

    F -->|No| H[Request Google Maps API]
    H --> I[Store eligible data in bank database]
    I --> J[Remove corresponding Redis pointer]
    J --> K[Return response to user]

    style B fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    style C fill:#fef3c7,stroke:#d97706,color:#92400e
    style H fill:#fef3c7,stroke:#d97706,color:#92400e
    style G fill:#dcfce7,stroke:#16a34a,color:#166534
    style J fill:#f3e8ff,stroke:#9333ea,color:#581c87

Next Challenge (stored information becomes outdated)

Eliminating unnecessary requests is only useful when the result returned to the user is still appropriate.

Location information changes. A branch may move, an ATM may close, and details about a place may be updated. Information that was accurate when first retrieved cannot be assumed to remain correct indefinitely.

Our reuse strategy therefore introduced another responsibility: managing freshness.

There are two obvious but undesirable extremes.

The first is to request fresh information every time. This may reduce reliance on older records, but it also sacrifices much of the performance and cost benefit of reuse, which was the basic goal.

The second is to keep using stored information indefinitely. That may look efficient in API usage statistics, but it risks showing users details that are no longer correct.

Neither approach is a good general policy.

Instead, data freshness needs to be treated as part of the serving strategy. A record must be considered usable only within the rules applicable to that information.

That requires distinguishing at least three things:

  • Availability: Does a record exist?
  • Freshness: Is the record still suitable to serve?
  • Permission: Is the application allowed to retain and reuse that information in this way?

These questions are related, but they are not interchangeable.

A record can exist in the database and still be too old to use. A record can be fresh but subject to a storage restriction. And a record may be permitted to persist even though some related location details require a different refresh policy.

The external provider's rules matter here. For example, Google explicitly permits storing Place IDs for later reuse and recommends refreshing IDs older than 12 months because they may change. That recommendation is distinct from the 30-day caching limit for Places API latitude and longitude values. Google's Place IDs documentation explains both.

This is why I wouldn't recommend a universal expiration period for every field in a location-related record. The correct policy depends on the source, field, use case, and applicable terms.

Redis adds another consideration. Even if the persistent record is refreshed or corrected, an old Redis entry must not continue to take precedence over the newer information.

The system needs a consistent strategy for both layers: determine whether the information is valid to serve, update or invalidate affected fast-lookup entries when necessary, and avoid reintroducing stale values through a later cache fill.

The precise mechanics can vary. The essential requirement is that Redis accelerates the correct lookup path rather than becoming a second, inconsistent source of truth.

This was an important part of the engineering problem. Reducing external requests is not a success if the application achieves it by returning information users should no longer see.

Why Redis lives in one feature

This decision deserves its own section, because "add Redis" is a tempting reflex.

We chose Redis for one feature with one specific performance requirement:

  • Latency matters here. People reach for a branch finder when they need an answer now, and a map that fills in slowly feels broken.
  • The access pattern fits. The feature repeatedly requests the same eligible records, which is exactly where a low-latency lookup layer pays off.
  • The two stores do different jobs. The database gives durability and a source of truth. Redis gives speed on hot records. Neither replaces the other.
  • Everywhere else, it would be pure cost. Putting Redis into unrelated parts of the system would add deployment, monitoring, failure modes, and invalidation rules without solving any problem those parts actually have.

That's the difference between a targeted engineering decision and adopting a tool because it's popular. We didn't need Redis everywhere. We needed the maps feature to retrieve the information it already had as quickly as possible.

The results: faster lookups, lower API spend

I'd rather describe what changed accurately than decorate it with numbers I can't defend.

  • Less unnecessary traffic to Google Maps, because lookups we could answer ourselves stopped becoming outbound requests.
  • Faster responses whenever information could be served from Redis or the database.
  • Reuse across repeat requests of eligible information that previously would have been fetched again.
  • Lower API spend, as avoidable requests were eliminated.
  • A growing body of stored, eligible records, which gives more repeat lookups the chance to be resolved without an external call.

That last point comes with an asterisk. Growth has limits: anything with a retention limit has to leave on schedule, and refreshes cost calls of their own (the ID-only refresh being the documented exception). New locations, data changes, usage patterns, and provider pricing all move the total. More stored information improves the opportunity for reuse. It doesn't promise that costs fall forever.

If you want to prove a change like this works rather than just believe it, these are the numbers to track, with a baseline captured before you ship:

  • Google Maps API request count.
  • Cost per period, or per relevant user activity.
  • Redis and database hit rates, where measurable.
  • Median and p95 response latency.
  • Duplicate requests on missing records.
  • Stale-data and refresh rate.

๐Ÿ“‰ 25% LOWER GOOGLE MAPS COSTS โ€” AND STILL FALLING

Merged: 10 August 2026

As of the publication date, Google Maps costs have dropped to just 25% of their previous level โ€” and continue to decrease.

Not because we switched providers. Not because we downgraded the experience.

Because we made a smarter engineering decision.


The broader lesson

The takeaway isn't "use Redis," and it isn't "store API responses." It's to understand your application's request patterns, then make deliberate choices about where data lives, when an external request is truly necessary, what can be reused, and how freshness is maintained.

For this feature, that meant combining five things:

  • Persistent storage of eligible data.
  • A feature-specific, low-latency lookup layer.
  • Correct handling of missing records.
  • Deliberate management of freshness.
  • Measurement of both performance and cost.

Each piece alone is a half-answer. Storage without freshness serves stale results. Speed without miss-handling multiplies calls. Neither means much if you can't measure it.

The biggest optimisation was not making an external API call faster. It was making fewer unnecessary calls, while returning the information users needed more quickly.

DISCUSS

If you found this helpful, and have such stories or smarter decisions, you or your team made, which really was super beneficial, please let me know in the comments and then discuss more engineering. Looking forword to your response felloz!!

Top comments (3)

Collapse
 
kyisaiah47 profile image
kyisaiah47 •

A stale Redis pointer can keep sending requests to Google after the database has the record. What invalidates that pointer if the database write succeeds but Redis deletion fails?

Collapse
 
sheraz4194 profile image
Sheraz Manzoor •

The database write and Redis invalidation are separate operations, so a failed deletion needs explicit handling. The robust solution is retryable invalidation with a TTL as a safety net, ensuring a stale pointer cannot persist indefinitely. Thanks for your question.

Collapse
 
abdullahgondal profile image
Abdullah Gondal •

Nice. One thing I didn't get is, why did you use the Radis only for this feature? I mean why not everywhere?