ExerciseDifficulty: Advanced2 min

Practice: A Caching Proxy for an Exchange Rate Service

The service is slow; the proxy keeps each rate for a minute.

A slow rate service is a natural Proxy, and this exercise asks you to name its kind, write it, and reason about a self-call. RemoteRateService.rate(pair) fetches a currency rate over the network. A rate may be reused for 60 seconds.

  1. Name the kind of proxy this is.
  2. Write CachingRateService with the same rate(pair) method. Take the clock as a parameter so a test can control the time.
  3. Requests arrive at 0, 10, 30 and 75 seconds. How many reach the remote service?
  4. If RemoteRateService had a rates(pairs) method that called self.rate for each pair, would those calls be cached?
Show a solution
py
from abc import ABC, abstractmethod
from typing import Callable


class RateService(ABC):
    @abstractmethod
    def rate(self, pair: str) -> float: ...


class RemoteRateService(RateService):
    def __init__(self) -> None:
        self.calls = 0

    def rate(self, pair: str) -> float:
        self.calls += 1
        print(f"remote: fetching {pair}")
        return {"GBP/EUR": 1.17, "GBP/USD": 1.27}[pair]


class CachingRateService(RateService):
    def __init__(self, service: RateService, clock: Callable[[], float], ttl_seconds: float) -> None:
        self._service = service
        self._clock = clock
        self._ttl = ttl_seconds
        self._cache: dict[str, tuple[float, float]] = {}

    def rate(self, pair: str) -> float:
        now = self._clock()
        cached = self._cache.get(pair)
        if cached is not None and now - cached[1] < self._ttl:
            return cached[0]
        value = self._service.rate(pair)
        self._cache[pair] = (value, now)
        return value


times = iter([0, 10, 30, 75])
remote = RemoteRateService()
rates = CachingRateService(remote, clock=lambda: next(times), ttl_seconds=60)
for _ in range(4):
    print(rates.rate("GBP/EUR"))
print(f"remote calls: {remote.calls}")
OutputRan with python:3.12-slim (Python 3.12.13)
remote: fetching GBP/EUR
1.17
1.17
1.17
remote: fetching GBP/EUR
1.17
remote calls: 2
Each rate is stored with the time it was fetched, and reused while it is under 60 seconds old.

It is a caching proxy. The requests at 10 and 30 seconds are under a minute after the fetch at 0, so they come from the cache. At 75 seconds the cached rate is 75 seconds old, so the proxy fetches again: two remote calls, as the output shows. Taking the clock as a parameter is what makes the answer exact rather than dependent on how fast the test runs.

The calls inside rates would not be cached. They are self-calls on the remote service and never pass through the proxy, so each would fetch. To cache them, give the proxy its own rates method that calls its own rate.