Optimizing System Resource Queries with the Proxy Pattern

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.

Tags: design-patterns cpp proxy-pattern performance-optimization Caching

Posted on Sun, 07 Jun 2026 17:09:41 +0000 by dino345