I have a Java class that has a Guava LoadingCache<String, Integer>
and in that cache, I'm planning to store two things: the average time active employees have worked for the day and their efficiency. I am caching these values because it would be expensive to compute every time a request comes in. Also, the contents of the cache will be refreshed (refreshAfterWrite
) every minute.
I was thinking of using a CacheLoader
for this situation, however, its load method only loads one value per key. In my CacheLoader
, I was planning to do something like:
private Service service = new Service();
public Integer load(String key) throws Exception {
if (key.equals("employeeAvg"))
return calculateEmployeeAvg(service.getAllEmployees());
if (key.equals("employeeEff"))
return calculateEmployeeEff(service.getAllEmployees());
return -1;
}
For me, I find this very inefficient since in order to load both values, I have to invoke service.getAllEmployees()
twice because, correct me if I'm wrong, CacheLoader
's should be stateless.
Which made me think to use the LoadingCache.put(key, value)
method so I can just create a utility method that invokes service.getAllEmployees()
once and calculate the values on the fly. However, if I do use LoadingCache.put()
, I won't have the refreshAfterWrite
feature since it's dependent on a cache loader.
How do I make this more efficient?
It seems like your problem stems from using strings to represent value types (Effective Java Item 50). Instead, consider defining a proper value type that stores this data, and use a memoizing
Supplier
to avoid recomputing them.You could even move these calculation methods inside
EmployeeStatistics
and simply pass in all employees to the constructor and let it compute the appropriate data.If you need to configure your caching behavior more than
Suppliers.memoize()
orSuppliers.memoizeWithExpiration()
can provide, consider this similar pattern, which hides the fact that you're using aCache
inside aSupplier
:I'm not sure, but you might be able to call it from inside the
load
method. I mean, compute the requested value as you do andput
in the other. However, this feels hacky.If
service.getAllEmployees
is expensive, then you could cache it. If bothcalculateEmployeeAvg
andcalculateEmployeeEff
are cheap, then recompute them when needed. Otherwise, it looks like you could use two caches.I guess, a method computing both values at once could be a reasonable solution. Create a tiny Pair-like class aggregating them and use it as the cache value. There'll be a single key only.
Concerning your own solution, it could be as trivial as
Instead of
synchronized
methods you may want to synchronize on a private final field. There are other possibilities (e.g.Atomic*
), but the basic design is probably simpler than adapting Guava'sCache
.Now, I see that there's
Suppliers#memoizeWithExpiration
in Guava. That's probably even simpler.