Smart caching in Hybris

Introduction

Performance matters. It’s good when a web site behaves fast because this improves user experience and reduces error rate due to timeouts and lack of resources. The Hybris platform addresses this aspect of system runtime by adding a cache layer between the database and models. If a model item or a query for models has been used recently and is used again then Hybris does not pass this call to the database and uses the cached data instead. It is an efficient way to reduce the load on the database and therefore increase performance. However, there is still a vast field of improvements because there are lots of overhead on the way of data from the database to a web page. This article outlines one source of this overhead and proposes a solution on how to eliminate it.

Problem Statement

Let’s look at what happens to data on its way from the database to a user. In general, it looks like this: data is read from the database into a model item, then the model item is converted to a data object, then the data object is passed to the representation layer and is sent to the user in response, usually as HTML or JSON content.

If there is no caching at all then the most time-consuming transition is the ‘DB to Model’ one. Fortunately, Hybris caches it. But how fast other transitions are? Unfortunately, it appears that the ‘Model to Data Object’ step is far from fast, too. In a recent Hybris-based project I took part in it took about 20-30 ms to convert a ProductModel item to a ProductData. Some other data (store/category/etc.) was involved, but anyway it is at least as long as 20 ms to convert data in a local system (which does not suffer from high load) even if Hybris does not go the database at all during this conversion! There is a number of reasons why it is so slow, but they all reside in the deep inside of the Hybris Jalo/Model layers and we don’t control them. What we can do here? We can take advantage of knowing that product data is read-only in most cases and is changed rarely at very specific points. So, it’s possible to create a global cache of ProductData objects so that there’s no need to convert the same data over and over again. And when data changes that affects any record of this cache we should just remove the expired cache records.

Solution

To be more specific, let’s continue with the case of optimizing the conversion of ProductModel to ProductData. Note, however, that observations below are generic and can be applied to any data that has the same usage pattern.

Let’s implement a lazy global cache first. It’s simple as the one below:

This cache transparently converts a ProductModel to a ProductData the first time and after that returns the cached ProductData for the same product code. The only thing to care about on caller’s side is to ensure that ProductData objects are not modified by it, which is usually the case.

To keep this cache up to date we must listen for modifications of items from which ProductData objects are built. For the sake of simplicity let it be just the correspondent ProductModel. How can we do this? Hybris has a number of the so-called interceptors – InitDefaultsInterceptor, PrepareInterceptor, ValidateInterceptor, LoadInterceptor, RemoveInterceptor. They are useful but they don’t allow for listening for model item modifications. To fill this gap, a custom AfterPersistInterceptor has been implemented (https://github.com/dzidzitop/hybris_after_persist_interceptor). It will be used later in this article. A simple clean-cache interceptor looks like this:

When a ProductModel is saved, this interceptor is called and just removes the correspondent ProductData from the global cache so that when this ProductModel has been converted again a new ProductData with fresh data is created and cached. Also, if a ProductModel is not changed then its cached ProductData is reused and never expires.

Performance Measurements

Now it’s time to check if all this brings any time/resource saving. Two JMeter scenarios have been created to measure this. In the first scenario, a single concurrent user is loading two different product details pages 100 times each. In the second scenario, there are 30 concurrent users who load two different product details pages 20 times each.

Limitations

There are things that can be thought of further.

First, the cache cleaning interceptor is not aware of the DB transactions and can expire cache records even if the product data modification is then rolled back. Also, there could be a data conflict if, say, a product import job changes the product within the transaction, saves it, then calls the converter, so that ProductData is cached and contains the modifications, and then rolls back the transaction. In this case, the ProductData cache record will contain wrong data, that was not actually imported to the database. Things like this are project-specific and should be addressed when there’s a complete understanding of how business data changes. Usually, this kind of conflicts can be avoided by convention, i.e. by prohibiting import jobs to use components that touch the cache of the data these jobs import.

The other limitation is that there is no track of the old values of items updated. When this is important then a BeforePersistInterceptor should be integrated into Hybris, with old values available in the interceptor context. The existing ValidateInterceptor can do this duty because it’s called right before items are persisted to the database. Its name is not perfect in this context but it’s essentially BeforePersistInterceptor.

Conclusion

In this article we observed a general data conversion flow in Hybris, identified that despite of Hybris has its own persistence data cache there is still room for further optimizations, which can be applied taking advantage of knowing the business domain and data usage patterns. As a demonstration of it, we implemented a caching solution for product data that is transparent for the callers (and thus is cheap to inject into a long-living or legacy system) and can be kept up-to-date accurately when product data changes.