Scenario: Exposing System Resource Usage via API
When building APIs that expose system metrics like CPU and memory utilization, performance optimization becomes critical. This article explores how the Proxy Pattern can efficiently handle resource monitoring requests.
The Challenge
Multiple servers may simultaneously call a resource monitoring endpoint. Each invocation traditionally triggers an operating system query, which is computationally expensive. The key insight: CPU and memory utilization values remain relatively stable within short time windows.
Naive Implemantation
The straightforward approach involves fetching fresh data on every request:
// Data structures
struct SystemMetrics {
float cpuUsage;
float memoryUsage;
long timestamp;
};
// Interface for metric retrieval
class SystemMetricsProvider {
public:
virtual SystemMetrics fetchMetrics() = 0;
};
// Concrete implementation
class RealMetricsProvider : public SystemMetricsProvider {
public:
SystemMetrics fetchMetrics() override {
SystemMetrics metrics;
metrics.cpuUsage = 0.45f;
metrics.memoryUsage = 0.23f;
return metrics;
}
};
While this works, it wastes resources by querying the OS on every call, even when values haven't changed significantly.
Attempted Cache Solution
A common improvement introduces caching directly into the provider:
class SystemMetricsProvider {
public:
SystemMetrics fetchMetrics() override {
if (!cachedMetrics.valid || isExpired()) {
cachedMetrics = RealMetricsProvider().fetchMetrics();
cachedMetrics.valid = true;
}
return cachedMetrics;
}
private:
bool isExpired() {
auto now = std::chrono::system_clock::now();
auto elapsed = std::chrono::duration_cast<:chrono::milliseconds>(
now.time_since_epoch()
).count();
return (elapsed - cachedMetrics.timestamp) > 2000;
}
SystemMetrics cachedMetrics;
};</:chrono::milliseconds>
This approach has limitations: caching logic becomes entangled with bussiness logic, and testing each component in isolation becomes difficult.
Applying the Proxy Pattern
The Proxy Pattern separates concerns by introducing an intermediary that manages caching while delegating actual data retrieval:
// Base interface defining the contract
class IMetricsProvider {
public:
virtual ~IMetricsProvider() = default;
virtual SystemMetrics retrieve() = 0;
};
// The actual implementation that fetches real data
class ActualMetricsProvider : public IMetricsProvider {
public:
SystemMetrics retrieve() override {
SystemMetrics metrics;
metrics.cpuUsage = 0.45f;
metrics.memoryUsage = 0.23f;
return metrics;
}
};
// Proxy that adds caching behavior
class CachingMetricsProxy : public IMetricsProvider {
public:
CachingMetricsProxy(IMetricsProvider* underlying)
: actualProvider(underlying), cacheValid(false) {}
SystemMetrics retrieve() override {
if (shouldRefresh()) {
cachedData = actualProvider->retrieve();
cachedData.timestamp = currentTimestamp();
cacheValid = true;
}
return cachedData;
}
private:
bool shouldRefresh() {
if (!cacheValid) return true;
return (currentTimestamp() - cachedData.timestamp) > 2000;
}
long currentTimestamp() {
auto tp = std::chrono::time_point_cast<:chrono::milliseconds>(
std::chrono::system_clock::now()
);
return std::chrono::duration_cast<:chrono::milliseconds>(
tp.time_since_epoch()
).count();
}
IMetricsProvider* actualProvider;
SystemMetrics cachedData;
bool cacheValid;
};</:chrono::milliseconds></:chrono::milliseconds>
Key Advantages
- Single Responsibility: The caching logic resides in the proxy, not in the core implementation
- Flexibility: Clients can use either the real provider or the cached proxy without code changes
- Testability: Each component can be unit tested independently
- Transparent Caching: From the client's perspective, both implementations follow the same interface
Usage Example
int main() {
IMetricsProvider* provider;
// Production: use cached version
provider = new CachingMetricsProxy(new ActualMetricsProvider());
// Testing: use actual version directly
// provider = new ActualMetricsProvider();
while (true) {
SystemMetrics data = provider->retrieve();
// Send data to client...
std::this_thread::sleep_for(std::chrono::seconds(2));
}
delete provider;
return 0;
}
This pattern proves valuable in scenarios involving expensive operations that benefit from temporal locality, such as system metrics, database queries, or external API cals.